在 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 对象中的最佳方法吗?我理想的解决方案将具有最少的样板文件,并且还有隐式引用语义而不是指针语义。
使用
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";
};
};
这可以通过在传递对象时将对象包装在
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`.
}
您需要处理四种情况:
int&
应该保留int&
int const&
应该变成int
int&&
应该变成int
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