Reputation: 40100
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.
Upvotes: 17
Views: 2006
Reputation: 217810
Your program is UB as you use dangling reference of the captured lambda.
So to perfect forward capture in lambda, you may use
template<class Callable>
auto discardable(Callable&& callable)
{
return [f = std::conditional_t<
std::is_lvalue_reference<Callable>::value,
std::reference_wrapper<std::remove_reference_t<Callable>>,
Callable>{std::forward<Callable>(callable)}]
{
std::forward<Callable>(f)();
};
}
It move-constructs the temporary lambda.
Upvotes: 6
Reputation: 21160
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
};
}
Upvotes: 13
Reputation: 136425
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()()&
.
Upvotes: 7