Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 233
Dean Avatar asked Sep 02 '26 17:09

Dean


2 Answers

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.

like image 161
NathanOliver Avatar answered Sep 04 '26 07:09

NathanOliver


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

like image 21
Jonathan S. Avatar answered Sep 04 '26 08:09

Jonathan S.



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!