12

What's the cleanest way to perfectly forward arguments into a lambda capture in C++20/C++23? By this I mean capturing rvalues by copy and lvalues by reference, inside of the coroutine object:

struct A { int _value{0}; };

auto foo = []<typename T>(T&& a) {
    return [a = std::forward<T>(a)]() mutable {
        ++a._value;
        std::cout << a._value << "\n";
    };
};

A my_a;
auto capture_as_lvalue = foo(my_a);
capture_as_lvalue();              // Prints `1`.
capture_as_lvalue();              // Prints `2`.
capture_as_lvalue();              // Prints `3`.
std::cout << my_a._value << "\n"; // Should print `3`.

auto capture_as_rvalue = foo(A{});
capture_as_rvalue(); // Prints `1`.

This answer seems to suggest that the above should work, but the above program (https://godbolt.org/z/Mz3caah5o) results in

1
2
3
0 <- should be 3
1

A blog post by Vittorio Romeo uses macros to achieve the desired effect. One downside is that the capture uses pointer semantics, rather than the implicit semantics of references. In this answer Fabio A. suggests a simpler method using deduction guides:

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

While this seems to result in the correct output, this does trigger the address sanitizer (https://godbolt.org/z/6heaxYEhE), and I'm not sure whether this is a false positive.

My question: is the suggestion by Fabio A. correct, and is it indeed the best way to perfectly capture variables into a lambda object? My ideal solution would have minimal boilerplate, and also implicit reference semantics rather than pointer semantics.

Mark
  • 1,306
  • 13
  • 19
  • 2
    Note that the mentioned solution talks about how to perfect-forward the arguments to the captures' constructors, it is not supposed to deduce whether to capture by reference or value. The lambda always holds its own captured objects, the forwarding just ensures efficient construction of them. – Quimby Jul 21 '22 at 12:36
  • 2
    Those semantics are insane; you get reference or value semantics based on the r/l value category of the call. There is a reason why `std::ref` exists; I'd mimic it here. Ie, `foo(std::ref(my_a))` would have the semantics of `foo(my_a)`, at least here the ref/value switch is **explicit** and not magical. – Yakk - Adam Nevraumont Jul 22 '22 at 19:47

3 Answers3

18

Use tuple to store arguments by reference or value depending on whether it is lvalue or rvalue (you can use std::apply to extend the variadic template version)

auto foo = []<typename T>(T&& a) {
    return [a = std::tuple<T>(std::forward<T>(a))]() mutable {
        ++std::get<0>(a)._value;
        std::cout << std::get<0>(a)._value << "\n";
    };
};

Demo

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
2

This can be solved by wrapping the object in a std::reference_wrapper when passing it in, though the object must be unwrapped within the lambda via std::unwrap_reference_t before accessing it. However, note here the use of perfect forwarding is removed in favour of using move semantics (which are supported uniformly by std::reference_wrapper).

#include <functional> 
#include <iostream>
#include <utility>

struct A { int _value{0}; };

auto foo = []<typename T>(T a) {
    return [a = std::move(a)]() {
        decltype(auto) b = std::unwrap_reference_t<T>(a);
        ++b._value;
        std::cout << b._value << "\n";
    };
};

int main()
{
    A my_a;
    auto capture_as_lvalue = foo(std::ref(my_a)); // Specify to use references at the call site.
    capture_as_lvalue();              // Prints `1`.
    capture_as_lvalue();              // Prints `2`.
    capture_as_lvalue();              // Prints `3`.
    std::cout << my_a._value << "\n"; // Will now print `3`.

    auto capture_as_rvalue = foo(A{});
    capture_as_rvalue(); // Prints `1`.
}
Antony Peacock
  • 449
  • 4
  • 8
  • Also in my opinion the correct solution. Your line `decltype(auto) b = std::unwrap_reference_t(a);` currently creates an unnecessary copy if T is not a `reference_wrapper`. There should be a lvalue reference added after the unwrap, otherwise `A` unnecessarily has to have a copy constructor. – Benjamin Buch Mar 18 '23 at 02:51
0

There are four cases you need to handle:

  1. int& should remain int&
  2. int const& should become int
  3. int&& should become int
  4. int const&& should become int

The answer of 康桓瑋 does handle only the cases 1 and 3. If you never have constant objects, this works and is sufficient. If you want to cover the const cases, then you can't get around a std::conditional.

#include <iostream>
#include <tuple>
#include <type_traits>

auto foo = []<typename T>(T&& ref) {
    using type = std::conditional_t<
        std::is_lvalue_reference_v<T>
        && !std::is_const_v<std::remove_reference_t<T>>,
        T, std::remove_cvref_t<T>>;
    return [wrapper = std::tuple<type>(std::forward<T>(ref))]() mutable {
        decltype(auto) v = std::get<0>(wrapper);
        return ++v;
    };
};

You can check the correct behavior as follows:

int main() {
    int a{0};
    auto capture = foo(a); // external reference
    std::cout << "a: int& -> int&\n";
    std::cout << "  captured expect 1 == " << capture() << '\n';
    std::cout << "  captured expect 2 == " << capture() << '\n';
    std::cout << "  original expect 2 == " << a << "\n";

    int const b{0};
    auto const_capture = foo(b); // internal copy
    std::cout << "b: const int& -> int\n";
    std::cout << "  captured expect 1 == " << const_capture() << '\n';
    std::cout << "  captured expect 2 == " << const_capture() << '\n';
    std::cout << "  original expect 0 == " << b << "\n";

    int c{0};
    auto move_capture = foo(std::move(c)); // internal copy
    std::cout << "c: int&& -> int\n";
    std::cout << "  captured expect 1 == " << move_capture() << '\n';
    std::cout << "  captured expect 2 == " << move_capture() << '\n';
    std::cout << "  original expect 0 == " << c << "\n";

    int const d{0};
    auto const_move_capture = foo(std::move(d)); // internal copy
    std::cout << "d: const int&& -> int\n";
    std::cout << "  captured expect 1 == " << const_move_capture() << '\n';
    std::cout << "  captured expect 2 == " << const_move_capture() << '\n';
    std::cout << "  original expect 0 == " << d << "\n";
}
a: int& -> int&
  captured expect 1 == 1
  captured expect 2 == 2
  original expect 2 == 2
b: const int& -> int
  captured expect 1 == 1
  captured expect 2 == 2
  original expect 0 == 0
c: int&& -> int
  captured expect 1 == 1
  captured expect 2 == 2
  original expect 0 == 0
d: const int&& -> int
  captured expect 1 == 1
  captured expect 2 == 2
  original expect 0 == 0

However, this construct is difficult for the user to understand and can easily be used incorrectly!


I would therefore advise you to instead leave it to the user to explicitly pass a reference wrapper if they want reference semantics. Your foo thus simplifies to a perfect capture with subsequent unwrapping of the wapper if it has been used.

#include <iostream>
#include <functional>

auto foo = []<typename T>(T&& v) {
    return [v_or_wrap = std::forward<T>(v)]() mutable {
        std::unwrap_reference_t<decltype(v_or_wrap)>& v = v_or_wrap;
        return ++v;
    };
};

Case 1 / a now behaves in the same way as the other three cases.

int main() {
    int a{0};
    auto capture = foo(a); // internal copy
    std::cout << "a: int& -> int\n";
    std::cout << "  captured expect 1 == " << capture() << '\n';
    std::cout << "  captured expect 2 == " << capture() << '\n';
    std::cout << "  original expect 0 == " << a << "\n";

    int const b{0};
    auto const_capture = foo(b); // internal copy
    std::cout << "b: const int& -> int\n";
    std::cout << "  captured expect 1 == " << const_capture() << '\n';
    std::cout << "  captured expect 2 == " << const_capture() << '\n';
    std::cout << "  original expect 0 == " << b << "\n";

    int c{0};
    auto move_capture = foo(std::move(c)); // internal copy
    std::cout << "c: int&& -> int\n";
    std::cout << "  captured expect 1 == " << move_capture() << '\n';
    std::cout << "  captured expect 2 == " << move_capture() << '\n';
    std::cout << "  original expect 0 == " << c << "\n";

    int const d{0};
    auto const_move_capture = foo(std::move(d)); // internal copy
    std::cout << "d: const int&& -> int\n";
    std::cout << "  captured expect 1 == " << const_move_capture() << '\n';
    std::cout << "  captured expect 2 == " << const_move_capture() << '\n';
    std::cout << "  original expect 0 == " << d << "\n";

To achieve the reference behavior you originally wanted as case 1 / a, the user must use a std::reference_wrapper instead. I have called this case z in the following.

    int z{0};
    auto ref_capture = foo(std::ref(z)); // external reference
    std::cout << "z: int& -> std::reference_wrapper<int>\n";
    std::cout << "  captured expect 1 == " << ref_capture() << '\n';
    std::cout << "  captured expect 2 == " << ref_capture() << '\n';
    std::cout << "  original expect 2 == " << z << "\n";
}
a: int& -> int
  captured expect 1 == 1
  captured expect 2 == 2
  original expect 0 == 0
b: const int& -> int
  captured expect 1 == 1
  captured expect 2 == 2
  original expect 0 == 0
c: int&& -> int
  captured expect 1 == 1
  captured expect 2 == 2
  original expect 0 == 0
d: const int&& -> int
  captured expect 1 == 1
  captured expect 2 == 2
  original expect 0 == 0
z: int& -> std::reference_wrapper<int>
  captured expect 1 == 1
  captured expect 2 == 2
  original expect 2 == 2
Benjamin Buch
  • 4,752
  • 7
  • 28
  • 51