为什么这个 lambda [=] 捕获会创建多个副本?

6
在以下代码中:
#include <iostream>
#include <thread>

using namespace std;

class tester {
public:
    tester() { 
        cout << "constructor\t" << this << "\n"; 
    }
    tester(const tester& other) { 
        cout << "copy cons.\t" << this << "\n"; 
    }
    ~tester() { 
        cout << "destructor\t" << this << "\n"; 
    }

    void print() const { 
        cout << "print\t\t" << this << "\n"; 
    }
};

int main() {
    tester t;

    cout << "  before lambda\n";
    thread t2([=] {
        cout << "  thread start\n";
        t.print();
        cout << "  thread end\n";
    });

    t2.join();
    cout << "  after join" << endl;
    
    return 0;
}

在Windows上使用cl.exe编译时,我得到以下结果:
constructor 012FFA93
  before lambda
copy cons.  012FFA92
copy cons.  014F6318
destructor  012FFA92
  thread start
print       014F6318
  thread end
destructor  014F6318
  after join
destructor  012FFA93

使用WSL上的g++编译器,我获得以下结果:

constructor     0x7ffff5b2155e
  before lambda
copy cons.      0x7ffff5b2155f
copy cons.      0x7ffff5b21517
copy cons.      0x7fffedc630c8
destructor      0x7ffff5b21517
destructor      0x7ffff5b2155f
  thread start
print           0x7fffedc630c8
  thread end
destructor      0x7fffedc630c8
  after join
destructor      0x7ffff5b2155e
  1. 我希望[=]捕获会创建恰好1个tester的副本。为什么会有多个立即被销毁的副本?

  2. 为什么MSVC和GCC存在差异?这是未定义行为还是其他原因?


12
Lambda本身是按值传递的,因此它连同其数据成员一起被复制,包括它按值捕获的内容。 - Igor Tandetnik
3
每当您的 lambda 表达式被复制时,它也会复制所有已捕获的对象。std::thread 必须以某种方式存储 lambda 表达式,这可能涉及复制。我相信闭包 可以 移动其捕获的对象,但是由于您的类型没有提供移动构造函数,因此在可以移动而不是复制的情况下,它被强制复制。如果您添加一个移动构造函数,那么您可能会看到较少的复制。 - François Andrieux
1
当然。当我提供一个移动构造函数时,第二个(和第三个使用GCC)副本就会变成移动。我猜测两个编译器之间的区别只是复制省略方面的差异? - MHebes
4
当你查看背景中的 λ-结构运作时,这会变得更加清晰:https://cppinsights.io/s/2c9bdb04 - Simon Kraemer
如果有人想回答,我会标记它的,谢谢大家。 - MHebes
显示剩余5条评论
1个回答

3
标准要求传递给std::thread构造函数的可调用对象必须是有效可复制的([thread.thread.constr])。
规定:以下均为真:
  • is_­constructible_­v<decay_­t<F>, F>
  • [...]
is_­constructible_­v<decay_­t<F>, F>is_copy_constructible相同(或者说,反过来也是一样)。
这允许实现自由地传递可调用对象,直到达到调用点。(事实上,标准本身建议至少复制一次可调用对象。)
由于lambda表达式被编译成一个重载了函数调用运算符的小型类(a functor),每次lambda表达式被复制时,它都会创建一个捕获的tester实例的副本。
如果您不希望发生复制,可以在捕获列表中使用对该实例的引用。
thread t2([&ref = t] {
    cout << "  thread start\n";
    ref.print();
    cout << "  thread end\n";
});

现场演示

输出:

constructor 0x7ffdfdf9d1e8
  before lambda
  thread start
print       0x7ffdfdf9d1e8
  thread end
  after join
destructor  0x7ffdfdf9d1e8

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接