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

问题描述 投票:0回答:3

在 C++20/C++23 中将参数完美转发到 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 seems 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

Vittorio Romeo 的一篇博文 使用宏来达到预期的效果。一个缺点是捕获使用指针语义,而不是引用的隐式语义。 在这个答案中 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. 的建议是否正确,这确实是将变量完美捕获到 lambda 对象中的最佳方法吗?我理想的解决方案将具有最少的样板文件,并且还有隐式引用语义而不是指针语义。

c++ lambda c++20 perfect-forwarding
3个回答
18
投票

使用

tuple
根据参数是左值还是右值,通过引用或值存储参数(您可以使用
std::apply
扩展 variadic 模板版本

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

演示


2
投票

这可以通过在传递对象时将对象包装在

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

0
投票

您需要处理四种情况:

  1. int&
    应该保留
    int&
  2. int const&
    应该变成
    int
  3. int&&
    应该变成
    int
  4. int const&&
    应该变成
    int

康桐薇的答案确实只处理情况 1 和 3。如果你从来没有常量对象,这就足够了。如果你想涵盖

const
案例,那么你就无法绕过
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;
    };
};

您可以检查正确的行为如下:

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

但是,这种结构对于用户来说很难理解并且很容易被错误使用!


因此,我建议您将它留给用户,如果他们想要引用语义,则显式传递引用包装器。您的

foo
因此简化为一个完美的捕获,如果它已被使用,随后打开wapper。

#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
© www.soinside.com 2019 - 2024. All rights reserved.