MHebes
MHebes

Reputation: 3197

Why does this lambda [=] capture create several copies?

In the following code:

#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;
}

When compiled with cl.exe (on Windows) I get the following:

constructor 012FFA93
  before lambda
copy cons.  012FFA92
copy cons.  014F6318
destructor  012FFA92
  thread start
print       014F6318
  thread end
destructor  014F6318
  after join
destructor  012FFA93

And with g++ (on WSL) I get:

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. I would expect that the [=] capture would create exactly 1 copy of tester. Why are there several copies that are immediately destroyed?

  2. Why the divergence between MSVC and GCC? Is this undefined behavior or something?

Upvotes: 6

Views: 275

Answers (1)

AndyG
AndyG

Reputation: 41090

The standard requires that the callable passed to the constructor for std::thread is effectively copy-constructible ([thread.thread.constr])

Mandates: The following are all true:

  • is_­constructible_­v<decay_­t<F>, F>
  • [...]

is_­constructible_­v<decay_­t<F>, F> is the same as is_copy_constructible (or rather, it's the other way around).

This is to allow implementations to freely pass around the callable until it reaches the point where it gets invoked. (In fact, the standard itself suggests the callable is copied at least once.)

Since a lambda is compiled into a small class with the function call operator overloaded (a functor), each time your lambda gets copied, it will create a copy of the captured tester instance.

If you do not wish for copying to happen, you can take a reference to your instance in the capture list instead:

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

Live Demo

Output:

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

Upvotes: 3

Related Questions