Reputation: 350
My class has a member of a type optional<A>
. I'm trying to implement function emplaceWhenReady
which gets list of parameters to A's constructor, but the non-trivial part is that A
can be only initialized after a certain event. When emplaceWhenReady
is invoked before the event, I need to somehow capture the initialization values.
For single constructor parameter the code can be written as:
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));
};
}
};
and _fInit()
can be now invoked when the class becomes _ready
. But I fail to write similar code for multiple parameters:
// 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)...);
};
}
}
Godbolt: https://godbolt.org/z/Fi3o1S
error: expected ',' or ']' in lambda capture list
_fInit = [this, argsCopy = std::forward<ARGS>(args)...]() {
^
Any help is appreciated!
Upvotes: 4
Views: 1476
Reputation: 158529
If we look at proposal p0780: Allow pack expansion in lambda init-capture it covers this problem and possible solutions:
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.
It discusses various solutions including using a tuple:
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:
This proposal was merged in the draft standard in March, so we should get this change in C++2a.
Upvotes: 3
Reputation: 350
With some help (see answer and comments above) the solution I was able to find is:
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);
};
}
}
Upvotes: 2