Search code examples
c++multithreadingclassc++11stdthread

std::thread initialization with class argument results with class object being copied multiple times


It seems that if you create an object of a class, and pass it to the std::thread initialization constructor, then the class object is constructed and destroyed as much as 4 times overall. My question is: could you explain, step by step, the output of this program? Why is the class being constructed, copy-constructed and destructed so many times in the process?

sample program:

#include <iostream>  
#include <cstdlib>
#include <ctime>
#include <thread>

class sampleClass {
public:
    int x = rand() % 100;
    sampleClass() {std::cout << "constructor called, x=" << x <<     std::endl;}
    sampleClass(const sampleClass &SC) {std::cout << "copy constructor called, x=" << x << std::endl;}
    ~sampleClass() {std::cout << "destructor called, x=" << x << std::endl;}
    void add_to_x() {x += rand() % 3;}
};

void sampleThread(sampleClass SC) {
    for (int i = 0; i < 1e8; ++i) { //give the thread something to do
        SC.add_to_x();
    }
    std::cout << "thread finished, x=" << SC.x << std::endl;
}

int main(int argc, char *argv[]) {
    srand (time(NULL));
    sampleClass SC;
    std::thread t1 (sampleThread, SC);
    std::cout << "thread spawned" << std::endl;
    t1.join();
    std::cout << "thread joined" << std::endl;
    return 0;
}

The output is:

constructor called, x=92
copy constructor called, x=36
copy constructor called, x=61
destructor called, x=36
thread spawned
copy constructor called, x=62
thread finished, x=100009889
destructor called, x=100009889
destructor called, x=61
thread joined
destructor called, x=92

compiled with gcc 4.9.2, no optimization.


Solution

  • There are a lot of copying/moving going on in the background. Note however, that neither the copy constructor nor the move constructor is called when the thread constructor is called.

    Consider a function like this:

    template<typename T> void foo(T&& arg);
    

    When you have r-value references to template arguments C++ treats this a bit special. I will just outline the rules here. When you call foo with an argument, the argument type will be

    • && - when the argument is an r-value
    • & - all other cases

    That is, either the argument will be passed as an r-value reference or a standard reference. Either way, no constructor will be invoked.

    Now look at the constructor of the thread object:

    template <class Fn, class... Args>
    explicit thread (Fn&& fn, Args&&... args);
    

    This constructor applies the same syntax, so arguments will never be copied/moved into the constructor arguments.

    The below code contains an example.

    #include <iostream>
    #include <thread>
    
    class Foo{
    public:
        int id;
    
        Foo()
        {
            id = 1;
            std::cout << "Default constructor, id = " << id << std::endl;
        }
    
        Foo(const Foo& f)
        {
            id = f.id + 1;
            std::cout << "Copy constructor, id = " << id << std::endl;
        }
    
        Foo(Foo&& f)
        {
            id = f.id;
            std::cout << "Move constructor, id = " << id << std::endl;
        }
    };
    
    void doNothing(Foo f)
    {
        std::cout << "doNothing\n";
    }
    
    template<typename T>
    void test(T&& arg)
    {
    }
    
    int main()
    {
        Foo f; // Default constructor is called
    
        test(f); // Note here that we see no prints from copy/move constructors
    
        std::cout << "About to create thread object\n";
        std::thread t{doNothing, f};
        t.join();
    
        return 0;
    }
    

    The output from this code is

    Default constructor, iCount = 1
    About to create thread object
    Copy constructor, id = 2
    Move constructor, id = 2
    Move constructor, id = 2
    doNothing
    
    • First, the object is created.
    • We call our test function just to see that nothing happens, no constructor calls.
    • Because we pass in an l-value to the thread constructor the argument has type l-value reference, hence the object is copied (with the copy constructor) into the thread object.
    • The object is moved into the underlying thread (managed by the thread object)
    • Object is finally moved into the thread-function doNothing's argument