f = std::forward in lambda capture, what does it mean?

196 Views Asked by At

I have the following code which is used to call a function on an object and pass any argument in a perfect-forwarding way

template <typename F, typename T>
inline auto call_with_args(F&& f, T&& t) {
    return [f = std::forward<F>(f), t = std::forward<T>(t)]
    (auto&&... args) mutable { return (t.*f)(std::forward<decltype(args)>(args)...); };
}

I'm trying to read this but I'm unsure what [f = std::forward<F>(f)] does. That is the 'capture by value' syntax, but what does that have to do with std::forward (which is a cast to rvalue reference from what I could read)? Is it re-defining f with a new variable which gets assigned whatever type and value category f had after being universal-reference-bound to the function parameter f?

Can somebody explain this to me in simple terms please?

2

There are 2 best solutions below

0
NathanOliver On BEST ANSWER

f and t are what as known as forwarding references and like all references, they are considered an lvalue, even if they refer to an rvalue.

So, since they are lvalues if you just captured them by value like

return [f, t](...) { ... };

Then f and t will be copied into the closure object as they are lvalues. This isn't great as we are missing out on being able to move f and t if they refer to rvalues . To avoid this we use std::forward which cast rvalues back to rvalues while leaving lvalues as lvalues.

That gives you the syntax of

return [f = std::forward<F>(f)](...) { ... };

which says create a member of the lambda named f and initialize it with std::forward<F>(f) where that f is the f from the surrounding scope.

4
Jonathan S. On

Let's go through it one by one:

  • If an lvalue reference is passed in, f = ... will have an lvalue reference as the .... A copy is made.
  • If an rvalue reference is passed in, f = ... will have an rvalue as the .... This means that the value is moved into the lambda capture instead.

As a result, the code saves on making a copy when it's passed an rvalue reference.

There's another way that achieves the same thing and is potentially a little less confusing to the reader: If you're going to keep the object anyway, just take it by value, then move.

template <typename F, typename T>
inline auto call_with_args(F f, T t) {
    return [f = std::move(f), t = std::move(t)]
    (auto&&... args) mutable { return (t.*f)(std::forward<decltype(args)>(args)...); };
}

That's a bit easier to reason about and achieves the same thing: A copy when an lvalue is passed in, and no copy when an rvalue (temporary) is passed in. (It does use one extra move, so if your objects are potentially expensive to move like std::array, you might want to stick to the original version.)