#c #lambda #c 17
Вопрос:
В следующем коде:
#include <iostream>
#include <thread>
using namespace std;
class tester {
public:
tester() {
cout << "constructort" << this << "n";
}
tester(const testeramp; other) {
cout << "copy cons.t" << this << "n";
}
~tester() {
cout << "destructort" << this << "n";
}
void print() const {
cout << "printtt" << this << "n";
}
};
int main() {
tester t;
cout << " before lambdan";
thread t2([=] {
cout << " thread startn";
t.print();
cout << " thread endn";
});
t2.join();
cout << " after join" << endl;
return 0;
}
При компиляции с cl.exe
(в Windows) Я получаю следующее:
constructor 012FFA93
before lambda
copy cons. 012FFA92
copy cons. 014F6318
destructor 012FFA92
thread start
print 014F6318
thread end
destructor 014F6318
after join
destructor 012FFA93
И с g
(на WSL) я получаю:
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 копиюtester
. Почему существует несколько копий, которые немедленно уничтожаются? - Почему расхождение между MSVC и GCC? Это неопределенное поведение или что-то в этом роде?
Комментарии:
1. Сама лямбда передается по значению. Таким образом, он копируется вместе со своими элементами данных, включая то, что он захватил по значению.
2. Всякий раз, когда ваша лямбда копируется, она также копирует все захваченные объекты со значениями.
std::thread
должен каким-то образом хранить лямбду, что может включать копии. Я считаю, что закрытие может перемещать захваченные объекты, но, поскольку вы не предоставили конструктор перемещения для своего типа, он вынужден копироваться, когда вместо этого его можно переместить. Если вы добавите конструктор перемещения, вы, вероятно, увидите меньше копий.3. Ах, конечно. Когда я предоставляю конструктор перемещений, 2-я (и 3-я копии w/ GCC) превращаются в перемещения. Разница между двумя компиляторами, как я предполагаю, заключается только в различиях в выборе копирования?
4. @MHebes Это становится более понятным, когда вы смотрите на лямбда-структуру, работающую в фоновом режиме: cppinsights.io/s/2c9bdb04
5. @SimonKraemer Вау, это потрясающий веб-сайт. Спасибо за ресурс.
Ответ №1:
Стандарт требует, чтобы вызываемый объект, для которого передается конструктору std::thread
, был эффективно сконструирован для копирования ([thread.thread.constr])
Мандаты: Все нижеперечисленное верно:
is_constructible_v<decay_t<F>, F>
- […]
is_constructible_v<decay_t<F>, F>
это то же самое, что is_copy_constructible
(или, скорее, все наоборот).
Это должно позволить реализациям свободно перемещаться по вызываемому объекту, пока он не достигнет точки, в которой он будет вызван. (На самом деле, сам стандарт предполагает, что вызываемый объект копируется по крайней мере один раз.)
Поскольку лямбда-код компилируется в небольшой класс с перегруженным оператором вызова функции (функтором), каждый раз, когда ваш лямбда-код копируется, он создает копию захваченного tester
экземпляра.
Если вы не хотите, чтобы копирование происходило, вы можете вместо этого указать ссылку на свой экземпляр в списке захвата:
thread t2([amp;ref = t] {
cout << " thread startn";
ref.print();
cout << " thread endn";
});
Живая демонстрация
Выход:
constructor 0x7ffdfdf9d1e8
before lambda
thread start
print 0x7ffdfdf9d1e8
thread end
after join
destructor 0x7ffdfdf9d1e8