Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

lambda

c++17

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?

like image 858
MHebes Avatar asked Sep 20 '21 15:09

MHebes


People also ask

Does lambda capture reference by value?

Lambdas always capture objects, and they can do so by value or by reference.

What is a lambda capture list?

The capture clause is used to (indirectly) give a lambda access to variables available in the surrounding scope that it normally would not have access to. All we need to do is list the entities we want to access from within the lambda as part of the capture clause.

What is mutable in lambda?

The mutable keyword is used so that the body of the lambda expression can modify its copies of the external variables x and y , which the lambda expression captures by value. Because the lambda expression captures the original variables x and y by value, their values remain 1 after the lambda executes.

How do you call a lambda function in C++?

A lambda is also just a function object, so you need to have a () to call it, there is no way around it (except of course some function that invokes the lambda like std::invoke ). If you want you can drop the () after the capture list, because your lambda doesn't take any parameters.


1 Answers

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
like image 67
AndyG Avatar answered Oct 18 '22 18:10

AndyG