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

bec*_*cca 16 c++ c++17

在下面的代码中,编译器有时似乎更喜欢调用模板化构造函数,并且在复制构造函数应该没问题时无法编译。行为似乎会根据值是否被捕获为 [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)

use*_*445 8

我不同意 \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}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
default constructed\n==== value capture ====\ncopy constructed\ntemplate constructed (const rvalue)\n==== init capture ====\ncopy constructed\nmove constructed\n
Run Code Online (Sandbox Code Playgroud)\n

那么这是怎么回事?首先,请注意,在这两种情况下,编译器都必须具体化一个临时 lambda 对象以传递到 的构造函数中fn_t。然后, 的构造函数fn_t必须复制 lambda 对象以保留它。(由于通常std::functionlambda 的寿命可能比传入其构造函数的 lambda 寿命长,因此它不能仅通过引用保留 lambda。)

\n

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

\n

在第二种情况(init capture)中,捕获的类型相当于声明中的t = t类型,即。因此,现在我们内部结构中的字段将是类型而不是,并且当必须移动复制 \ 构造函数的类型参数时,编译器将选择\的正常移动构造函数来移动该字段。tnewauto tnew = ttracerLAMBDA_Ttracerconst tracerLAMBDA_T&&fn_ttracer

\n


康桓瑋*_*康桓瑋 6

对于[v], lambda 内部成员变量的类型vconst 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&&