Capturing perfectly-forwarded variable in lambda

16,843

Solution 1

Is it correct to capture the perfectly-forwarded mStuff variable with the &mStuff syntax?

Yes, assuming that you don't use this lambda outside doSomething. Your code captures mStuff per reference and will correctly forward it inside the lambda.

For mStuff being a parameter pack it suffices to use a simple-capture with a pack-expansion:

template <typename... T> void doSomething(T&&... mStuff)
{
    auto lambda = [&mStuff...]{ doStuff(std::forward<T>(mStuff)...); };
}

The lambda captures every element of mStuff per reference. The closure-object saves an lvalue reference for to each argument, regardless of its value category. Perfect forwarding still works; In fact, there isn't even a difference because named rvalue references would be lvalues anyway.

Solution 2

To make the lambda valid outside the scope where it's created, you need a wrapper class that handles lvalues and rvalues differently, i.e., keeps a reference to an lvalue, but makes a copy of (by moving) an rvalue.

Header file capture.h:

#pragma once

#include <type_traits>
#include <utility>

template < typename T >
class capture_wrapper
{
   static_assert(not std::is_rvalue_reference<T>{},"");
   std::remove_const_t<T> mutable val_;
public:
   constexpr explicit capture_wrapper(T&& v)
      noexcept(std::is_nothrow_move_constructible<std::remove_const_t<T>>{})
   :val_(std::move(v)){}
   constexpr T&& get() const noexcept { return std::move(val_); }
};

template < typename T >
class capture_wrapper<T&>
{
   T& ref_;
public:
   constexpr explicit capture_wrapper(T& r) noexcept : ref_(r){}
   constexpr T& get() const noexcept { return ref_; }
};

template < typename T >
constexpr typename std::enable_if<
   std::is_lvalue_reference<T>{},
   capture_wrapper<T>
>::type
capture(std::remove_reference_t<T>& t) noexcept
{
   return capture_wrapper<T>(t);
}

template < typename T >
constexpr typename std::enable_if<
   std::is_rvalue_reference<T&&>{},
   capture_wrapper<std::remove_reference_t<T>>
>::type
capture(std::remove_reference_t<T>&& t)
   noexcept(std::is_nothrow_constructible<capture_wrapper<std::remove_reference_t<T>>,T&&>{})
{
   return capture_wrapper<std::remove_reference_t<T>>(std::move(t));
}

template < typename T >
constexpr typename std::enable_if<
   std::is_rvalue_reference<T&&>{},
   capture_wrapper<std::remove_reference_t<T>>
>::type
capture(std::remove_reference_t<T>& t)
   noexcept(std::is_nothrow_constructible<capture_wrapper<std::remove_reference_t<T>>,T&&>{})
{
   return capture_wrapper<std::remove_reference_t<T>>(std::move(t));
}

Example/test code that shows it works. Note that the "bar" example shows how one can use std::tuple<...> to work around the lack of pack expansion in lambda capture initializer, useful for variadic capture.

#include <cassert>
#include <tuple>
#include "capture.h"

template < typename T >
auto foo(T&& t)
{
   return [t = capture<T>(t)]()->decltype(auto)
   {
      auto&& x = t.get();
      return std::forward<decltype(x)>(x);
      // or simply, return t.get();
   };
}

template < std::size_t... I, typename... T >
auto bar_impl(std::index_sequence<I...>, T&&... t)
{
   static_assert(std::is_same<std::index_sequence<I...>,std::index_sequence_for<T...>>{},"");
   return [t = std::make_tuple(capture<T>(t)...)]()
   {
      return std::forward_as_tuple(std::get<I>(t).get()...);
   };
}
template < typename... T >
auto bar(T&&... t)
{
   return bar_impl(std::index_sequence_for<T...>{}, std::forward<T>(t)...);
}

int main()
{
   static_assert(std::is_same<decltype(foo(0)()),int&&>{}, "");
   assert(foo(0)() == 0);

   auto i = 0;
   static_assert(std::is_same<decltype(foo(i)()),int&>{}, "");
   assert(&foo(i)() == &i);

   const auto j = 0;
   static_assert(std::is_same<decltype(foo(j)()),const int&>{}, "");
   assert(&foo(j)() == &j);

   const auto&& k = 0;
   static_assert(std::is_same<decltype(foo(std::move(k))()),const int&&>{}, "");
   assert(foo(std::move(k))() == k);

   auto t = bar(0,i,j,std::move(k))();
   static_assert(std::is_same<decltype(t),std::tuple<int&&,int&,const int&,const int&&>>{}, "");
   assert(std::get<0>(t) == 0);
   assert(&std::get<1>(t) == &i);
   assert(&std::get<2>(t) == &j);
   assert(std::get<3>(t) == k and &std::get<3>(t) != &k);

}

Solution 3

TTBOMK, for C++14, I think the above solutions for lifetime handling can be simplified to:

template <typename T> capture { T value; }

template <typename T>
auto capture_example(T&& value) {
  capture<T> cap{std::forward<T>(value)};
  return [cap = std::move(cap)]() { /* use cap.value *; };
};

or more anonymous:

template <typename T>
auto capture_example(T&& value) {
  struct { T value; } cap{std::forward<T>(value)};
  return [cap = std::move(cap)]() { /* use cap.value *; };
};

Used it here (admittedly, this particular block of code is rather useless :P)

https://github.com/EricCousineau-TRI/repro/blob/3fda1e0/cpp/generator.cc#L161-L176

Solution 4

Yes you can do perfect capturing, but not directly. You will need to wrap the type in another class:

#define REQUIRES(...) class=std::enable_if_t<(__VA_ARGS__)>

template<class T>
struct wrapper
{
    T value;
    template<class X, REQUIRES(std::is_convertible<T, X>())>
    wrapper(X&& x) : value(std::forward<X>(x))
    {}

    T get() const
    {
        return std::move(value);
    }
};

template<class T>
auto make_wrapper(T&& x)
{
    return wrapper<T>(std::forward<T>(x));
}

Then pass them as parameters to a lambda that returns a nested lambda that captures the parameters by value:

template<class... Ts>
auto do_something(Ts&&... xs)
{
    auto lambda = [](auto... ws)
    {
        return [=]()
        {
            // Use `.get()` to unwrap the value
            some_other_function(ws.get()...);
        };
    }(make_wrapper(std::forward<Ts>(xs)...));

    lambda();
}

Solution 5

Here's a solution for C++17 that uses deduction guides to make it easy. I'm elaborating on Vittorio Romeo's (the OP) blog post, where he provides a solution to his own question.

std::tuple can be used to wrap the perfectly forwarded variables, making a copy or keeping a reference of each of them on a per-variable basis, as needed. The tuple itself is value-captured by the lambda.

To make it easier and cleaner, I'm going to create a new type derived from std::tuple, so to provide guided construction (that will let us avoid the std::forward and decltype() boilerplate) and pointer-like accessors in case there's just one variable to capture.

// This is the generic case
template <typename... T>
struct forwarder: public std::tuple<T...> {
    using std::tuple<T...>::tuple;        
};

// This is the case when just one variable is being captured.
template <typename T>
struct forwarder<T>: public std::tuple<T> {
    using std::tuple<T>::tuple;

    // Pointer-like accessors
    auto &operator *() {
        return std::get<0>(*this);
    }

    const auto &operator *() const {
        return std::get<0>(*this);
    }

    auto *operator ->() {
        return &std::get<0>(*this);
    }

    const auto *operator ->() const {
        return &std::get<0>(*this);
    }
};

// std::tuple_size needs to be specialized for our type, 
// so that std::apply can be used.
namespace std {
    template <typename... T>
    struct tuple_size<forwarder<T...>>: tuple_size<tuple<T...>> {};
}

// The below two functions declarations are used by the deduction guide
// to determine whether to copy or reference the variable
template <typename T>
T forwarder_type(const T&);

template <typename T>
T& forwarder_type(T&);

// Here comes the deduction guide
template <typename... T>
forwarder(T&&... t) -> forwarder<decltype(forwarder_type(std::forward<T>(t)))...>;

And then one can use it like following.

The variadic version:

// Increment each parameter by 1 at each invocation and print it.
// Rvalues will be copied, Lvalues will be passed as references.
auto variadic_incrementer = [](auto&&... a)
{
    return [a = forwarder(a...)]() mutable 
    { 
        std::apply([](auto &&... args) {
            (++args._value,...);
            ((std::cout << "variadic_incrementer: " << args._value << "\n"),...);
        }, a);
    };
};

The non-variadic version:

// Increment the parameter by 1 at each invocation and print it.
// Rvalues will be copied, Lvalues will be passed as references.
auto single_incrementer = [](auto&& a)
{
    return [a = forwarder(a)]() mutable 
    { 
        ++a->_value;
        std::cout << "single_incrementer: " << a->_value << "\n";
    };
};
Share:
16,843
Vittorio Romeo
Author by

Vittorio Romeo

I write code, lift weights and play games. I also like everything sci-fi.

Updated on July 08, 2022

Comments

  • Vittorio Romeo
    Vittorio Romeo almost 2 years
    template<typename T> void doSomething(T&& mStuff)
    {
        auto lambda([&mStuff]{ doStuff(std::forward<T>(mStuff)); });
        lambda();
    }
    

    Is it correct to capture the perfectly-forwarded mStuff variable with the &mStuff syntax?

    Or is there a specific capture syntax for perfectly-forwarded variables?

    EDIT: What if the perfectly-forwarded variable is a parameter pack?

  • Yakk - Adam Nevraumont
    Yakk - Adam Nevraumont over 9 years
    In the first case, within the lambda you have lvalue references to everything. And you have the strange fact that your lambda is only valid until you leave the current scope. Both mean that it isn't a general solution to the OP's problem. In the second and third case, you capture by-value, which also isn't the same as perfect forwarding (you do perfect forward into the by-value capture). In the forth case, it is similar, but no perfect forwarding occurs. In short, none of them are perfect analogues for "perfect forwarding". Try forward_as_tuple maybe?
  • Columbo
    Columbo over 9 years
    @Yakk I did rewrite my answer. However: Saving lvalue references is exactly what should be done here, forward_as_tuple is out of place. The "strange" fact that the lambda is only valid until I leave the scope is somehow self-explanatory considering the fact that it captures by reference.
  • Yakk - Adam Nevraumont
    Yakk - Adam Nevraumont over 9 years
    even capturing references by reference has that problem: technically the lambda is only valid until thr variable, not the referenced value, expires! I suspect some std::ref shenanigans are required die to annoyances around lambda quirks.
  • Columbo
    Columbo over 9 years
    @Yakk What exactly do you mean by "technically the lambda is only valid until thr variable, not the referenced value, expires!"? I have the vague feeling I missed some important nonsensical rule about Lambdas here
  • Yakk - Adam Nevraumont
    Yakk - Adam Nevraumont over 9 years
    the reference capture lifetime rule in the standard references captured variables, not data, and their scope. This allows an actual practical optimization (capture stack pointer only), which makes it a slight question if it is a defect or not.
  • Columbo
    Columbo over 9 years
    @Yakk Ahh, now I understand what you mean (I knew of this optimization earlier).
  • Columbo
    Columbo over 9 years
    @Yakk So where exactly lies the problem? doSomething can take rvalues, so using the lambda outside is, in general, problematic anyway. And I mentioned that you cannot use it outside doSomething.
  • Yakk - Adam Nevraumont
    Yakk - Adam Nevraumont over 9 years
    std::tuple<T> where T&& is deduced gives you values for rvalues and references for lvalues and can be safely returned, and corresponds to how manual function objects returned from functions with perfect forwarding are usually implemented.
  • Columbo
    Columbo over 9 years
    @Yakk Yes, but in this case it's not clear from the question whether he wants to actually take the lambda outside. If he doesn't, unnecessary moves could occur.
  • Yongwei Wu
    Yongwei Wu over 9 years
    With C++14, the new lambda capture expression will be helpful. Check out the use of wrapper in curry at nvwa.cvs.sourceforge.net/viewvc/nvwa/nvwa/…. My test shows using this reduces the number of constructor calls.
  • T.C.
    T.C. about 9 years
  • Eric Cousineau
    Eric Cousineau over 5 years
  • haelix
    haelix about 5 years
    @Yakk-AdamNevraumont one of your comments made me ask stackoverflow.com/q/55559308/1088790
  • Toby Brull
    Toby Brull almost 5 years
    Or even shorter [cap = capture<T> {std::forward<T> (value)}] { /* use cap.value */ }
  • Toby Brull
    Toby Brull almost 5 years
    Can also use std::tuple instead of the bespoke capture type: [cap = std::tuple<T> (std::forward<T> (value))] { /* use std::get<0> (cap) */ }