捕获参数包的副本

Capturing a copy of parameter pack

我的 class 有一个 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));
      };
    }
};
当 class 变为 _ready 时,现在可以调用

_fInit()。但是我没有为多个参数编写类似的代码:

// 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)...]() {
                                                          ^

感谢任何帮助!

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

With the introduction of generalized lambda capture [1], lambda captures can be nearly arbitrarily complex and solve nearly all problems. However, there is still an awkward hole in the capabilities of lambda capture when it comes to parameter packs: you can only capture packs by copy, by reference, or by... std::tuple?

Consider the simple example of trying to wrap a function and its arguments into a callable to be accessed later. If we copy everything, the implementation is both easy to write and read:

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...);
    };
}

But if we try to be more efficient about the implementation and try to move all the arguments into the lambda? It seems like you should be able to use an init-capture and write:

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...);
    };
}

But this runs afoul of very explicit wording from [expr.prim.lambda.capture]/17, emphasis mine:

A simple-capture followed by an ellipsis is a pack expansion. An init-capture followed by an ellipsis is ill-formed.

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

As a result of this restriction, our only option is to put all the args... into a std::tuple. But once we do that, we don't have access to the arguments as a parameter pack, so we need to pull them back out of the tuple in the body, using something like 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);
    };
}

Which gets even worse if what we wanted to do with that captured parameter pack was invoke a named function rather than a captured object. At that point, all semblance of comprehension goes out the window:

这个提议是 merged in the draft standard in March,所以我们应该在 C++2a 中得到这个改变。

在一些帮助下(见上面的回答和评论)我找到的解决方案是:

    template<typename... ARGS>
    void emplaceWhenReady(ARGS&&... args) {
      if (_ready) {
        _a.emplace(std::forward<ARGS>(args)...);
      } else {
        _fInit = [this, argsCopy = std::make_tuple(std::forward<ARGS>(args)...)]() {
            auto doEmplace = [&](auto&... params) {
                _a.emplace(std::move(params)...);
            };
            std::apply(doEmplace, argsCopy);
        };
      }
    }