Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Perfect forwarding of a callable

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.

  1. Is it well-defined?
  2. Am I guaranteed that callable is never copied and never used after its lifetime has ended?

This is tagged C++14, but applies to all following standards.

like image 779
YSC Avatar asked Jan 29 '19 10:01

YSC


People also ask

What is perfect forwarding?

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.

What is the point of std :: forward?

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.”


2 Answers

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
    };
}
like image 119
Passer By Avatar answered Oct 14 '22 06:10

Passer By


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()()&.

like image 39
Maxim Egorushkin Avatar answered Oct 14 '22 08:10

Maxim Egorushkin