首页 > 解决方案 > 捕获参数包的副本

问题描述

我的班级有一个 type 的成员optional<A>。我正在尝试实现emplaceWhenReady获取 A 的构造函数的参数列表的函数,但重要的部分是A只能在某个事件之后初始化。什么时候emplaceWhenReady事件之前调用时,我需要以某种方式捕获初始化值。

对于单个构造函数参数,代码可以写成:

struct B {
   bool _ready;
   std::optional<A> _a;
   std::function<void()> _fInit;

   template<typename ARG>
   void emplaceWhenReady1(ARG&& arg) {
      if (_ready) {
         _a.emplace(std::forward<ARG>(arg));
      } else {
         _fInit = [this, argCopy = std::forward<ARG>(arg)]() {
            _a.emplace(std::move(argCopy));
      };
    }
};

并且_fInit()现在可以在类变为_ready. 但是我没有为多个参数编写类似的代码:

// Fails to compile
template<typename... ARGS>
void emplaceWhenReady(ARGS&&... args) {
    if (_ready) {
        _a.emplace(std::forward<ARGS>(args)...);
    } else {
        _fInit = [this, argsCopy = std::forward<ARGS>(args)...]() {
            _a.emplace(std::move(argsCopy)...);
        };
    }
}

神箭:https ://godbolt.org/z/Fi3o1S

error: expected ',' or ']' in lambda capture list
       _fInit = [this, argsCopy = std::forward<ARGS>(args)...]() {
                                                          ^

任何帮助表示赞赏!

标签: c++lambdacapture

解决方案


如果我们看一下提案p0780: Allow pack expansion in lambda init-capture,它涵盖了这个问题和可能的解决方案:

随着广义 lambda 捕获 [1] 的引入,lambda 捕获几乎可以任意复杂并解决几乎所有问题。但是,当涉及到参数包时,lambda 捕获的功能仍然存在一个尴尬的漏洞:您只能通过复制、引用或... std::tuple 来捕获包?

考虑一个简单的例子,试图将一个函数及其参数包装成一个可调用的,以便稍后访问。如果我们复制所有内容,则该实现既易于编写又易于阅读:

template<class F, class... Args>
auto delay_invoke(F f, Args... args) {
    // the capture here can also be just [=]
    return [f, args...]() -> decltype(auto) {
        return std::invoke(f, args...);
    };
}

但是,如果我们尝试提高实现效率并尝试将所有参数移动到 lambda 中呢?看来您应该能够使用初始化捕获并编写:

template<class F, class... Args>
auto delay_invoke(F f, Args... args) {
    return [f=std::move(f), ...args=std::move(args)]() -> decltype(auto) {
        return std::invoke(f, args...);
    };
}

但这与 [expr.prim.lambda.capture]/17 中非常明确的措辞相冲突,强调我的:

后跟省略号的简单捕获是包扩展。后跟省略号的初始化捕获是格式错误的。

它讨论了各种解决方案,包括使用元组:

由于这个限制,我们唯一的选择是将所有 args... 放入 std::tuple 中。但是一旦我们这样做了,我们就无法将参数作为参数包访问,因此我们需要使用 std::apply() 之类的方法将它们从主体中的元组中拉回:

template<class F, class... Args>
auto delay_invoke(F f, Args... args) {
    return [f=std::move(f), tup=std::make_tuple(std::move(args)...)]() -> decltype(auto) {
        return std::apply(f, tup);
    };
}

如果我们想要对捕获的参数包执行的操作是调用命名函数而不是捕获的对象,情况会变得更糟。在那一点上,所有的理解表面都消失了:

这个提议在 3 月份被合并到标准草案中,所以我们应该在 C++2a 中得到这个改变。


推荐阅读