Perfectly forwarding lambda capture in C++20 (or newer)

Viewed 453

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.

2 Answers

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

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`.
}
Related