将不可复制的闭包对象传递给 std::function 参数

Posted

技术标签:

【中文标题】将不可复制的闭包对象传递给 std::function 参数【英文标题】:Passing a non-copyable closure object to std::function parameter [duplicate] 【发布时间】:2014-01-17 13:24:50 【问题描述】:

在 C++14 中,lambda 表达式可以通过使用捕获初始化程序从变量中移动来捕获变量。但是,这使得生成的闭包对象不可复制。如果我有一个接受std::function 参数(我无法更改)的现有函数,我无法传递闭包对象,因为std::function 的构造函数要求给定函子是CopyConstructible

#include <iostream>
#include <memory>

void doit(std::function<void()> f) 
    f();


int main()

    std::unique_ptr<int> p(new int(5));
    doit([p = std::move(p)] ()  std::cout << *p << std::endl; );

这会产生以下错误:

/usr/bin/../lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/functional:1911:10: error: 
      call to implicitly-deleted copy constructor of '<lambda at test.cpp:10:7>'
            new _Functor(*__source._M_access<_Functor*>());
                ^        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/bin/../lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/functional:1946:8: note: in
      instantiation of member function 'std::_Function_base::_Base_manager<<lambda at test.cpp:10:7>
      >::_M_clone' requested here
              _M_clone(__dest, __source, _Local_storage());
              ^
/usr/bin/../lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/functional:2457:33: note: in
      instantiation of member function 'std::_Function_base::_Base_manager<<lambda at test.cpp:10:7>
      >::_M_manager' requested here
            _M_manager = &_My_handler::_M_manager;
                                       ^
test.cpp:10:7: note: in instantiation of function template specialization 'std::function<void
      ()>::function<<lambda at test.cpp:10:7>, void>' requested here
        doit([p = std::move(p)] ()  std::cout << *p << std::endl; );
             ^
test.cpp:10:8: note: copy constructor of '' is implicitly deleted because field '' has a deleted
      copy constructor
        doit([p = std::move(p)] ()  std::cout << *p << std::endl; );
              ^
/usr/bin/../lib/gcc/x86_64-linux-gnu/4.8/../../../../include/c++/4.8/bits/unique_ptr.h:273:7: note: 
      'unique_ptr' has been explicitly marked deleted here
      unique_ptr(const unique_ptr&) = delete;
      ^

是否有合理的解决方法?

使用 Ubuntu clang 版本 3.5-1~exp1 (trunk) 进行测试

【问题讨论】:

这是我在代理中遇到的越来越多的问题(上次它出现在我看来是以通用方式实现 PIMPL)。问题是一个对象的能力是由它的类型决定的(这里std::function&lt;void()&gt;)所以如果你想要一个只有在传递的对象是可复制的情况下才可复制的对象,如果传递的对象是可移动的,则可移动的,等等......不幸的是不可能使用相同的类型 => 我猜一个位掩码作为另一个模板参数 (std::function&lt;void(), MoveConstructible&gt;) 可以,但它可能很闷:/ 我有我自己的用于此目的的类型擦除持有人。据我所知,没有任何改善这种情况的建议,尽管我被告知有来自国家机构的 cmets 讨论这个话题。 可能std::function_ref 能够处理这个open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0792r2.html。否则使用std::cref 就像下面的答案之一。 【参考方案1】:

有这个办法:

template< typename signature >
struct make_copyable_function_helper;
template< typename R, typename... Args >
struct make_copyable_function_helper<R(Args...)> 
  template<typename input>
  std::function<R(Args...)> operator()( input&& i ) const 
    auto ptr = std::make_shared< typename std::decay<input>::type >( std::forward<input>(i) );
    return [ptr]( Args... args )->R 
      return (*ptr)(std::forward<Args>(args)...);
    ;
  
;

template< typename signature, typename input >
std::function<signature> make_copyable_function( input && i ) 
  return make_copyable_function_helper<signature>()( std::forward<input>(i) );

我们创建一个指向我们数据的共享指针,然后创建一个可复制的 lambda 来捕获该共享指针,然后我们将该可复制的 lambda 包装到所请求签名的 std::function 中。

在上述情况下,您只需:

doit( make_copyable_function<void()>( [p = std::move(p)] ()  std::cout << *p << std::endl;  ) );

稍微高级一点的版本推迟了类型擦除,并添加了一层完美转发以减少开销:

template<typename input>
struct copyable_function 
  typedef typename std::decay<input>::type stored_input;
  template<typename... Args>
  auto operator()( Args&&... args )->
    decltype( std::declval<input&>()(std::forward<Args>(args)...) )
  
    return (*ptr)(std::forward<Args>(args));
  
  copyable_function( input&& i ):ptr( std::make_shared<stored_input>( std::forward<input>(i) ) ) 
  copyable_function( copyable_function const& ) = default;
private:
  std::shared_ptr<stored_input> ptr;
;
template<typename input>
copyable_function<input> make_copyable_function( input&& i ) 
  return std::forward<input>(i); 

这不需要您传递签名,并且在某些情况下可能会稍微更有效,但使用了更晦涩的技术。

在 C++14 中可以更简洁:

template< class F >
auto make_copyable_function( F&& f ) 
  using dF=std::decay_t<F>;
  auto spf = std::make_shared<dF>( std::forward<F>(f) );
  return [spf](auto&&... args)->decltype(auto) 
    return (*spf)( decltype(args)(args)... );
  ;

完全不需要辅助类型。

【讨论】:

【参考方案2】:

如果闭包对象的生命周期不是问题,您可以在引用包装器中传递它:

int main()

    std::unique_ptr<int> p(new int(5));
    auto f = [p = std::move(p)]
        std::cout << *p << std::endl;
    ;
    doit(std::cref(f));

这显然不适用于每一个场景,但它适用于您的示例程序。

编辑:看一眼 N3797(C++14 工作草案)§ 20.9.11.2.1 [func.wrap.func.con] p7,CopyConstructible 要求仍然存在。我想知道是否有不能放松到MoveConstructible 的技术原因,还是委员会没有解决这个问题?

编辑:回答我自己的问题:std::functionCopyConstructible,所以包装的函子也需要是 CopyConstructible

【讨论】:

如果对象的闭包不是问题,你就不能让 doit 接受一个 const 函数&? @MadScienceDreams 如果该实现避免实例化std::function 的复制构造函数,它可以与将函数对象构造为std::function&lt;void()&gt; barstd::move(f); 的特定实现一起使用。但它不是可移植的行为,因为std::function 明确要求仿函数在 [func.wrap.func.con]/7 中是可复制构造的。确实,it fails with gcc 4.8.1, for example. @MadScienceDreams 我假设std::function 的实现通过使用包含“虚拟复制构造函数”的抽象接口包装传递的函子来实现类型擦除。我熟悉的每个 C++ 实现都会实例化声明为虚拟的模板的所有成员以填充 vtable,即使标准说未使用的虚拟成员的实例化是特定于实现的。显然,虚拟复制构造函数是传递给std::function 的函子的CopyConstructible 要求的来源。 C++0x 草稿并不总是有CopyConstructible 的要求,它是由LWG 1287 添加的。实现不必使用任何虚函数(GCC 的实现也不需要),但类型擦除仍然意味着如果目标可能不可复制,您就无法提供可复制的包装器。【参考方案3】:

如果你知道你实际上并不打算复制你的函数对象,那么你可以将它包装在一个让编译器认为它是可复制的类型中:

struct ThrowOnCopy 
  ThrowOnCopy() = default;
  ThrowOnCopy(const ThrowOnCopy&)  throw std::logic_error("Oops!"); 
  ThrowOnCopy(ThrowOnCopy&&) = default;
  ThrowOnCopy& operator=(ThrowOnCopy&&) = default;
;

template<typename T>
  struct FakeCopyable : ThrowOnCopy
  
    FakeCopyable(T&& t) : target(std::forward<T>(t))  

    FakeCopyable(FakeCopyable&&) = default;

    FakeCopyable(const FakeCopyable& other)
    : ThrowOnCopy(other),                             // this will throw
      target(std::move(const_cast<T&>(other.target))) // never reached
     

    template<typename... Args>
      auto operator()(Args&&... a)
       return target(std::forward<Args>(a)...); 

    T target;
  ;


template<typename T>
  FakeCopyable<T>
  fake_copyable(T&& t)
   return  std::forward<T>(t) ; 


// ...

doit( fake_copyable([p = std::move(p)] ()  std::cout << *p << std::endl; ) );

函数模板fake_copyable根据编译器(和&lt;type_traits&gt;)创建一个包装器CopyConstructible,但不能在运行时复制。

如果您将FakeCopyable&lt;X&gt; 存储在std::function 中,然后最终复制std::function,您将收到std::logic_error,但如果您只移动std::function,一切都会正常。

target(std::move(const_cast&lt;T&amp;&gt;(other.target))) 看起来令人担忧,但该初始化程序永远不会运行,因为基类初始化程序将首先抛出。所以令人担忧的const_cast 从来没有真正发生过,它只是让编译器开心。

【讨论】:

此解决方案要求T 是默认可构造的。 @Sogartar 很好,谢谢。我已经修复了代码,现在它可以工作了。

以上是关于将不可复制的闭包对象传递给 std::function 参数的主要内容,如果未能解决你的问题,请参考以下文章

当我们将对象作为参数传递给方法时,为啥会调用复制构造函数?

将对象作为参数传递给构造函数并将其属性复制到新对象?

如何通过原始指针将闭包作为参数传递给 C 函数?

php [php:closure]将参数传递给#php中的闭包

如何将参数传递给我的处理程序

(打字稿)试图将字体真棒图标作为对象传递给子组件时遇到问题