C++令人困惑的闭包捕获 [v] 与 [v = v]

  • 本文关键字:闭包 C++ c++ c++17
  • 更新时间 :
  • 英文 :


在下面的代码中,编译器似乎有时更喜欢调用模板化构造函数,并且在复制构造函数应该很好时无法编译。行为似乎会根据值是捕获为 [v] 还是 [v = v] 而改变,我认为它们应该是完全相同的事情。我错过了什么?

我正在使用 gcc 11.2.0 并使用"g++ 文件.cpp-std=C++17"编译它

#include <functional>
#include <iostream>
#include <string>
using namespace std;
template <class T>
struct record {
explicit record(const T& v) : value(v) {}
record(const record& other) = default;
record(record&& other) = default;
template <class U>
record(U&& v) : value(forward<U>(v)) {} // Removing out this constructor fixes print1
string value;
};
void call(const std::function<void()>& func) { func(); }
void print1(const record<string>& v) {
call([v]() { cout << v.value << endl; }); // This does not compile, why?
}
void print2(const record<string>& v) {
call([v = v]() { cout << v.value << endl; }); // this compiles fine
}
int main() {
record<string> v("yo");
print1(v);
return 0;
}

我不反对康桓瑋的回答,但我发现有点难以理解,所以让我用另一个例子来解释一下。 请考虑以下程序:

#include <functional>
#include <iostream>
#include <typeinfo>
#include <type_traits>
struct tracer {
tracer() { std::cout << "default constructedn"; }
tracer(const tracer &) { std::cout << "copy constructedn"; }
tracer(tracer &&) { std::cout << "move constructedn"; }
template<typename T> tracer(T &&t) {
if constexpr (std::is_same_v<T, const tracer>)
std::cout << "template constructed (const rvalue)n";
else if constexpr (std::is_same_v<T, tracer&>)
std::cout << "template constructed (lvalue)n";
else
std::cout << "template constructed (other ["
<< typeid(T).name() << "])n";
}
};
int
main()
{
using fn_t = std::function<void()>;
const tracer t;
std::cout << "==== value capture ====n";
fn_t([t]() {});
std::cout << "==== init capture ====n";
fn_t([t = t]() {});
}

运行时,此程序输出以下内容:

default constructed
==== value capture ====
copy constructed
template constructed (const rvalue)
==== init capture ====
copy constructed
move constructed

这到底是怎么回事呢? 首先,请注意,在这两种情况下,编译器都必须具体化一个临时 lambda 对象以传递到构造函数中以进行fn_t。 然后,fn_t的构造函数必须创建 lambda 对象的副本以保留它。 (由于通常std::function可能比传递给其构造函数的 lambda 更持久,因此它不能仅通过引用保留 lambda。

在第一种情况下(值捕获),捕获t的类型正好是t的类型,即const tracer。 因此,您可以将 lambda 对象的未命名类型视为某种编译器定义的struct,其中包含类型为const tracer的字段。 让我们给这个结构起一个假名LAMBDA_T. 因此,构造fn_t函数的参数是LAMBDA_T&&类型,因此访问内部字段的表达式是const tracer&&类型,它比实际的复制构造函数更匹配模板构造函数的转发引用。 (在重载解析中,当两者都可用时,右值更喜欢绑定到右值引用,而不是绑定到常量左值引用。

在第二种情况下(init capture),捕获t = t的类型等效于像auto tnew = t这样的声明中的tnew类型,即tracer。 所以现在我们内部LAMBDA_T结构中的字段将是tracer类型而不是const tracer,当必须移动复制fn_t构造函数的LAMBDA_T&&类型的参数时,编译器将选择tracer的正常移动构造函数来移动该字段。

对于[v],lambda 内部成员变量的类型vconst record,所以当你

void call(const std::function<void()>&);
void print1(const record<string>& v) {
call([v] { });
}

由于[v] {}是一个 prvalue,当它初始化const std::function&时,v将被复制const record&&,并且模板构造函数将被选中,因为它不受约束。

为了调用v的复制构造函数,你可以

void call(const std::function<void()>&);
void print1(const record<string>& v) {
auto l = [v] { };
call(l);
}

对于[v=v],lambda 内部的成员变量v的类型是record,所以当 prvalue lambda 初始化std::function时,它将直接调用record的 move 构造函数,因为record&&更好的匹配。

最新更新