Почему этот лямбда-захват [=] создает несколько копий?

#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. Я ожидал бы, что [=] захват создаст ровно 1 копию tester . Почему существует несколько копий, которые немедленно уничтожаются?
  2. Почему расхождение между 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