在下面的代码中,编译器有时似乎更喜欢调用模板化构造函数,并且在复制构造函数应该没问题时无法编译。行为似乎会根据值是否被捕获为 [v] 或 [v = v] 而改变,我认为这些应该是完全相同的事情。我缺少什么?
我正在使用 gcc 11.2.0 并使用“g++ file.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;
}
Run Code Online (Sandbox Code Playgroud)
我不同意 \xe5\xba\xb7\xe6\xa1\x93\xe7\x91\x8b\ 的答案,但我发现它有点难以理解,所以让我用一个不同的例子来解释它。考虑以下程序:
\n#include <functional>\n#include <iostream>\n#include <typeinfo>\n#include <type_traits>\n\nstruct tracer {\n tracer() { std::cout << "default constructed\\n"; }\n tracer(const tracer &) { std::cout << "copy constructed\\n"; }\n tracer(tracer &&) { std::cout << "move constructed\\n"; }\n template<typename T> tracer(T &&t) {\n if constexpr (std::is_same_v<T, const tracer>)\n std::cout << "template constructed (const rvalue)\\n";\n else if constexpr (std::is_same_v<T, tracer&>)\n std::cout << "template constructed (lvalue)\\n";\n else\n std::cout << "template constructed (other ["\n << typeid(T).name() << "])\\n";\n }\n};\n\nint\nmain()\n{\n using fn_t = std::function<void()>;\n\n const tracer t;\n std::cout << "==== value capture ====\\n";\n fn_t([t]() {});\n std::cout << "==== init capture ====\\n";\n fn_t([t = t]() {});\n}\nRun Code Online (Sandbox Code Playgroud)\n运行时,该程序输出以下内容:
\ndefault constructed\n==== value capture ====\ncopy constructed\ntemplate constructed (const rvalue)\n==== init capture ====\ncopy constructed\nmove constructed\nRun Code Online (Sandbox Code Playgroud)\n那么这是怎么回事?首先,请注意,在这两种情况下,编译器都必须具体化一个临时 lambda 对象以传递到 的构造函数中fn_t。然后, 的构造函数fn_t必须复制 lambda 对象以保留它。(由于通常std::functionlambda 的寿命可能比传入其构造函数的 lambda 寿命长,因此它不能仅通过引用保留 lambda。)
第一种情况(值捕获),捕获的类型t正是 的类型t,即const tracer。因此,您可以将 lambda 对象的未命名类型视为某种编译器定义的struct包含类型字段的类型const tracer。让我们给这个结构一个假名LAMBDA_T。因此,构造函数的参数fn_t是 类型LAMBDA_T&&,并且访问内部字段的表达式因此是 类型const tracer&&,它比实际的复制构造函数更好地匹配模板构造函数的转发引用。(在重载决策中,当两者都可用时,右值更喜欢绑定到右值引用,而不是绑定到 const 左值引用。)
在第二种情况(init capture)中,捕获的类型相当于声明中的t = t类型,即。因此,现在我们内部结构中的字段将是类型而不是,并且当必须移动复制 \ 构造函数的类型参数时,编译器将选择\的正常移动构造函数来移动该字段。tnewauto tnew = ttracerLAMBDA_Ttracerconst tracerLAMBDA_T&&fn_ttracer
对于[v], lambda 内部成员变量的类型v是const record,所以当您
void call(const std::function<void()>&);
void print1(const record<string>& v) {
call([v] { });
}
Run Code Online (Sandbox Code Playgroud)
由于[v] {}是纯右值,当它初始化 时const std::function&,v将被复制const record&&,并且将选择模板构造函数,因为它不受约束。
为了调用 的v复制构造函数,您可以这样做
void call(const std::function<void()>&);
void print1(const record<string>& v) {
auto l = [v] { };
call(l);
}
Run Code Online (Sandbox Code Playgroud)
因为, lambda内部的[v=v]成员变量的类型是,所以当纯右值lambda初始化时,它会直接调用 的移动构造函数,因为这样匹配更好。vrecordstd::functionrecordrecord&&