在C++20(或更新版本)中完美转发lambda捕获

mcdcgff0  于 2023-03-20  发布在  其他
关注(0)|答案(3)|浏览(123)

在C20/C23中,将参数完美地转发到lambda捕获中的最干净的方法是什么?这里我指的是在协程对象内部通过复制捕获右值,通过引用捕获左值:

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似乎表明上述程序应该可以工作,但上面的程序(https://godbolt.org/z/Mz3caah5o)导致

1
2
3
0 <- should be 3
1

Vittorio罗密欧的A blog post使用宏来实现所需的效果。一个缺点是捕获使用指针语义,而不是引用的隐式语义。In this answer Fabio A.建议使用推导向导的更简单的方法:

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

虽然这看起来会产生正确的输出,但这确实会触发地址清理程序(https://godbolt.org/z/6heaxYEhE),我不确定这是否是误报。
我的问题:Fabio A.的建议正确吗?2它真的是完美地将变量捕获到lambda对象中的最佳方法吗?3我理想的解决方案应该有最少的样板,以及隐式引用语义而不是指针语义。

5ktev3wc

5ktev3wc1#

使用tuple通过引用或值来存储参数,具体取决于它是左值还是右值(可以使用std::apply来扩展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

yqlxgs2m

yqlxgs2m2#

这可以通过在传入对象时将其 Package 在std::reference_wrapper中来解决,尽管在访问对象之前必须通过std::unwrap_reference_t在lambda中展开对象。然而,请注意,这里删除了完全转发的使用,而使用了移动语义(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`.
}
8ljdwjyq

8ljdwjyq3#

您需要处理四种情况:

  1. int&应保持为int&
  2. int const&应变为int
  3. int&&应变为int
  4. int const&&应变为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;
    };
};

您可以按如下方式检查是否正确:
x一个一个一个一个x一个一个二个x

但是,用户很难理解此结构,并且很容易使用错误!

因此,我建议您,如果用户需要引用语义,就应该让他们显式地传递引用 Package 器,这样,foo就可以简化为一个完美的捕获过程,如果使用了wapper,就可以随后对其进行解 Package 。

#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;
    };
};

案例1 / a现在的行为方式与其他三种案例相同。

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";

为了实现你最初想要的示例1 / a的引用行为,用户必须使用std::reference_wrapper来代替,我在下面将这个示例称为z。

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

相关问题