如何从移动捕获 lambda 表达式创建 std::function?

How to create an std::function from a move-capturing lambda expression?

提问人:seertaak 提问时间:8/21/2014 最后编辑:Walterseertaak 更新时间:7/9/2019 访问量:13306

问:

我正在尝试创建一个从移动捕获 lambda 表达式。请注意,我可以毫无问题地创建一个移动捕获 lambda 表达式;只有当我尝试将其包装在 an 中时,我才会出现错误。std::functionstd::function

例如:

auto pi = std::make_unique<int>(0);

// no problems here!
auto foo = [q = std::move(pi)] {
    *q = 5;
    std::cout << *q << std::endl;
};

// All of the attempts below yield:
// "Call to implicitly-deleted copy constructor of '<lambda...."

std::function<void()> bar = foo;
std::function<void()> bar{foo};
std::function<void()> bar{std::move(foo)};
std::function<void()> bar = std::move(foo);
std::function<void()> bar{std::forward<std::function<void()>>(foo)};
std::function<void()> bar = std::forward<std::function<void()>>(foo);

我会解释为什么我想写这样的东西。我编写了一个 UI 库,类似于 jQuery 或 JavaFX,它允许用户通过将 s 传递给名称为 、 等的方法来处理鼠标/键盘事件。std::functionon_mouse_down()on_mouse_drag()push_undo_action()

显然,我想传入的最好使用移动捕获的 lambda 表达式,否则我需要求助于我在 C++1 为标准时使用的丑陋的“释放/获取 lambda”习语:std::function

std::function<void()> baz = [q = pi.release()] {
    std::unique_ptr<int> p{q};
    *p = 5;
    std::cout << *q << std::endl;
};

请注意,在上面的代码中,调用两次将是一个错误。但是,在我的代码中,此闭包可以保证只调用一次。baz

顺便说一句,在我的真实代码中,我不是在传递一个 ,而是传递一些更有趣的东西。std::unique_ptr<int>

最后,我使用的是 Xcode6-Beta4,它使用以下版本的 clang:

Apple LLVM version 5.1 (clang-503.0.40) (based on LLVM 3.4svn)
Target: x86_64-apple-darwin13.3.0
Thread model: posix
C++ Lambda 标准 C++14

评论

6赞 T.C. 8/21/2014
你不能。 要求函数对象为 。std::functionCopyConstructible
4赞 Manu343726 8/21/2014
这与 stackoverflow.com/questions/25330716/ 非常相似......另外,为什么不只使用函数的模板而不是类型擦除呢?用作通用函数类型不是一个好主意。std::functionstd::function
0赞 seertaak 8/21/2014
好吧,这个想法是为了避免编译时间过长,再加上在 UI 回调的上下文中使用性能损失是可以接受的。(也许是过早的优化!std::function
0赞 maxschlepzig 10/15/2018
相关问题:std::function 的仅移动版本
0赞 Oliv 12/13/2018
现在包含在标准中的范围库使用半规则包装器修复了这个问题(此处仅用于向后兼容):eel.is/c++draft/range.semi.wrap

答:

44赞 Robert Allan Hennigan Leahy 8/21/2014 #1

template<class F> function(F f);

template <class F, class A> function(allocator_arg_t, const A& a, F f);

要求:应为 。 应用于参数类型和返回类型。A 的复制构造函数和析构函数不应抛出异常。FCopyConstructiblefCallableArgTypesR

§20.9.11.2.1 [func.wrap.func.con]

请注意,它是根据此构造函数 和 定义的,因此适用相同的限制:operator =swap

template<class F> function& operator=(F&& f);

影响: function(std::forward<F>(f)).swap(*this);

§20.9.11.2.1 [func.wrap.func.con]

所以回答你的问题:是的,可以从移动捕获的 lambda 构造一个(因为这仅指定 lambda 如何捕获),但不可能从仅移动类型构造一个(例如,移动捕获 lambda 移动捕获不可复制构造的东西)。std::functionstd::function

评论

0赞 seertaak 8/21/2014
我不明白你最后一句话中的第一句和第二句之间的区别(“可以构造......”和“但这是不可能的......”)。您能举个例子吗?
3赞 Robert Allan Hennigan Leahy 8/21/2014
@seertaak:假设你有一个 lambda,它通过移动捕获一个。被移动到 lambda 中,但 是 ,因此 lambda 是可复制的,即使它被 move 捕获。因此,在其他条件相同的情况下,这个 lambda 可用于构造一个 .也就是说,你如何捕获某些东西并不真正相关,重要的是这些对象在捕获后赋予 lambda 的属性。std::vectorstd::vectorstd::vectorCopyConstructiblestd::function
0赞 seertaak 8/21/2014
等待:ints 是可复制构造的,就像 std::vector 一样。那为什么它不应该在 ints 上工作呢?
2赞 Ivan Aksamentov - Drop 8/21/2014
@seertaak你捕获的,而不是 .尝试将其替换为可复制构造的。std::unique_ptrintstd::shared_ptr
3赞 Ivan Aksamentov - Drop 8/21/2014
@seertaak 另请参阅此处对为什么 on-Earth 要求函子可复制构造的解释以及通过适配器的解决方法。std::function
39赞 Yakk - Adam Nevraumont 8/25/2014 #2

由于必须对存储的可调用对象的复制构造函数进行类型擦除,因此不能从仅移动类型构造它。您的 lambda 是仅移动类型,因为它按值捕获仅移动类型。所以。。。你无法解决你的问题。 无法存储您的 lambda。std::function<?>std::function

至少不是直接的。

这是C++,我们只是绕过问题。

template<class F>
struct shared_function {
  std::shared_ptr<F> f;
  shared_function() = delete; // = default works, but I don't use it
  shared_function(F&& f_):f(std::make_shared<F>(std::move(f_))){}
  shared_function(shared_function const&)=default;
  shared_function(shared_function&&)=default;
  shared_function& operator=(shared_function const&)=default;
  shared_function& operator=(shared_function&&)=default;
  template<class...As>
  auto operator()(As&&...as) const {
    return (*f)(std::forward<As>(as)...);
  }
};
template<class F>
shared_function< std::decay_t<F> > make_shared_function( F&& f ) {
  return { std::forward<F>(f) };
}

现在已经完成了上述工作,我们可以解决您的问题。

auto pi = std::make_unique<int>(0);

auto foo = [q = std::move(pi)] {
  *q = 5;
  std::cout << *q << std::endl;
};

std::function< void() > test = make_shared_function( std::move(foo) );
test(); // prints 5

a 的语义与其他函数略有不同,因为它的副本与原始函数共享相同的状态(包括变成 时)。shared_functionstd::function

我们还可以编写一个 move-only fire-once 函数:

template<class Sig>
struct fire_once;

template<class T>
struct emplace_as {};

template<class R, class...Args>
struct fire_once<R(Args...)> {
  // can be default ctored and moved:
  fire_once() = default;
  fire_once(fire_once&&)=default;
  fire_once& operator=(fire_once&&)=default;

  // implicitly create from a type that can be compatibly invoked
  // and isn't a fire_once itself
  template<class F,
    std::enable_if_t<!std::is_same<std::decay_t<F>, fire_once>{}, int> =0,
    std::enable_if_t<
      std::is_convertible<std::result_of_t<std::decay_t<F>&(Args...)>, R>{}
      || std::is_same<R, void>{},
      int
    > =0
  >
  fire_once( F&& f ):
    fire_once( emplace_as<std::decay_t<F>>{}, std::forward<F>(f) )
  {}
  // emplacement construct using the emplace_as tag type:
  template<class F, class...FArgs>
  fire_once( emplace_as<F>, FArgs&&...fargs ) {
    rebind<F>(std::forward<FArgs>(fargs)...);
  }
  // invoke in the case where R is not void:
  template<class R2=R,
    std::enable_if_t<!std::is_same<R2, void>{}, int> = 0
  >
  R2 operator()(Args...args)&&{
    try {
      R2 ret = invoke( ptr.get(), std::forward<Args>(args)... );
      clear();
      return ret;
    } catch(...) {
      clear();
      throw;
    }
  }
  // invoke in the case where R is void:
  template<class R2=R,
    std::enable_if_t<std::is_same<R2, void>{}, int> = 0
  >
  R2 operator()(Args...args)&&{
    try {
      invoke( ptr.get(), std::forward<Args>(args)... );
      clear();
    } catch(...) {
      clear();
      throw;
    }
  }

  // empty the fire_once:
  void clear() {
    invoke = nullptr;
    ptr.reset();
  }

  // test if it is non-empty:
  explicit operator bool()const{return (bool)ptr;}

  // change what the fire_once contains:
  template<class F, class...FArgs>
  void rebind( FArgs&&... fargs ) {
    clear();
    auto pf = std::make_unique<F>(std::forward<FArgs>(fargs)...);
    invoke = +[](void* pf, Args...args)->R {
      return (*(F*)pf)(std::forward<Args>(args)...);
    };
    ptr = {
      pf.release(),
      [](void* pf){
        delete (F*)(pf);
      }
    };
  }
private:
  // storage.  A unique pointer with deleter
  // and an invoker function pointer:
  std::unique_ptr<void, void(*)(void*)> ptr{nullptr, +[](void*){}};
  void(*invoke)(void*, Args...) = nullptr;
};

它甚至通过标签支持不可移动的类型。emplace_as<T>

活生生的例子

请注意,您必须在右值上下文中进行评估(即,在 a 之后),因为无声破坏性似乎很粗鲁。()std::move()

这个实现不使用 SBO,因为如果这样做,它将要求存储的类型是可移动的,并且启动(对我来说)会有更多的工作。

评论

0赞 user877329 4/30/2020
为什么调用fire_once函数两次会是个问题?只要它持有可调用对象,调用对象应该没问题,对吧?
0赞 Yakk - Adam Nevraumont 5/1/2020
@user8是一个玩具的例子;调用时无效的函数,无法再次调用。代理移动构造函数更普遍,以及复制成本高于消耗的任何情况。[x=std::optional<T>(x)]()mutable{ auto r = std::move(*x); x=std::nullopt; return r; }
7赞 Taylor 7/9/2019 #3

下面是一个更简单的解决方案:

   auto pi = std::make_unique<int>(0);

   auto ppi = std::make_shared<std::unique_ptr<int>>(std::move(pi));

   std::function<void()> bar = [ppi] {
        **ppi = 5;
        std::cout << **ppi << std::endl;
   };

现场示例在这里

评论

3赞 Evg 11/13/2019
本次演讲中也提到了类似的技巧。
2赞 Adamski 7/2/2020
多亏了@Taylor这被证明是一个非常棘手的难题
1赞 Christophe 9/21/2020
@Evg 谢谢你分享这个演讲。从那次演讲中,现在我知道可以用于这种情况。这节省了很多时间。folly::Function