I came up with the following code to transform a R()
-like into a void()
-like callable:
#include <utility>
template<class Callable>
auto discardable(Callable&& callable)
{ return [&]() { (void) std::forward<Callable>(callable)(); }; }
// ^-- is it ok?
int main()
{
auto f = discardable([n=42]() mutable { return n--; });
f();
}
I'm worried about the capture by reference.
callable
is never copied and never used after its lifetime has ended?This is tagged C++14, but applies to all following standards.
Perfect forwarding allows a template function that accepts a set of arguments to forward these arguments to another function whilst retaining the lvalue or rvalue nature of the original function arguments.
std::forward has a single use case: to cast a templated function parameter (inside the function) to the value category (lvalue or rvalue) the caller used to pass it. This allows rvalue arguments to be passed on as rvalues, and lvalues to be passed on as lvalues, a scheme called “perfect forwarding.”
Lambdas are anonymous structs with an operator()
, the capture list is a fancy way of specifying the type of its members. Capturing by reference really is just what it sounds like: you have reference members. It isn't hard to see the reference dangles.
This is a case where you specifically don't want to perfectly forward: you have different semantics depending on whether the argument is a lvalue or rvalue reference.
template<class Callable>
auto discardable(Callable& callable)
{
return [&]() mutable { (void) callable(); };
}
template<class Callable>
auto discardable(Callable&& callable)
{
return [callable = std::forward<Callable>(callable)]() mutable { // move, don't copy
(void) std::move(callable)(); // If you want rvalue semantics
};
}
Since callable
can be an xvalue there is a chance that it gets destroyed before the lambda capture, hence leaving you with a dangling reference in the capture. To prevent that, if an argument is an r-value it needs to be copied.
A working example:
template<class Callable>
auto discardable(Callable&& callable) { // This one makes a copy of the temporary.
return [callable = std::move(callable)]() mutable {
static_cast<void>(static_cast<Callable&&>(callable)());
};
}
template<class Callable>
auto discardable(Callable& callable) {
return [&callable]() mutable {
static_cast<void>(callable());
};
}
You can still face lifetime issues if callable
is an l-value reference but its lifetime scope is smaller than that of the lambda capture returned by discardable
. So, it may be the safest and easiest to always move or copy callable
.
As a side note, although there are new specialised utilities that perfect-forward the value category of the function object, like std::apply
, the standard library algorithms always copy function objects by accepting them by value. So that if one overloaded both operator()()&
and operator()()&&
the standard library would always use operator()()&
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With