4

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.

Marcin L
  • 73
  • 10
  • I edited the example, int x is initialized as rand()%100 so you can see when which object is constructed /destroyed – Marcin L Dec 13 '15 at 19:04
  • As far as I see, you construct your object once, then you pass it to the thread object, which copy-constructs it once again when accepting your argument, then the thread object passes it to your function, which because it takes the class by value, copy constructs the argument once again. Btw, try to add a move constructor and see what happens then! I suppose that all these copies happen because you don't have a move constructor! Look up "C++ rule of five" – notadam Dec 13 '15 at 19:12
  • Also, why would you use no optimization when compiling? I'm sure the compiler would optimize out these redundant copies. – notadam Dec 13 '15 at 19:21
  • @adam10603 Move doesn't seem good solution because in the target application I am spawning several threads of the same kind, each with a copy of the same class (copies need to be independent). As far as I understand, moving leaves the original object in indetermined state. As for optimization - example wasn't optimized exactly so that compiler doesn't mess with the code too much, anyway even with `-O3` the output is the same. – Marcin L Dec 13 '15 at 20:32
  • 1
    What I meant is that you would create as many objects as your number of threads, and you move those into the threads, avoiding copying – notadam Dec 13 '15 at 20:35

2 Answers2

1

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
jensa
  • 2,792
  • 2
  • 21
  • 36
0
int main(int argc, char *argv[]) {
    sampleClass SC; // default constructor
    std::thread t1 (sampleThread, SC); // Two copies inside thread constructor,
                                       //use std::ref(SC) to avoit it
    //..
}

void sampleThread(sampleClass SC) { // copy SC: pass by ref to avoid it
                                // but then modifications are for original and not the copy
  // ...
}

Fixed version Demo

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • Unfortunately for my application std::ref is not an option, as I'm launching more threads of the same kind and before I do, there's a lot of work to setup the class being passed, so I intentionally want to copy construct it. However it still remains unclear to me, why `std::thread t1 (sampleThread, SC);` creates 2 copies? – Marcin L Dec 13 '15 at 19:53
  • @MarcinL: http://en.cppreference.com/w/cpp/thread/thread/thread, see `3)`: `decay_copy` does one copy construct (as there is no move in your case) in the return, and an other where it is called. I think that some implementations may reduce the number of copies to one. – Jarod42 Dec 13 '15 at 20:13
  • Is it then possible to avoid memory leaks, assuming that you want to spawn several threads with independent copies of a huge class (that needs to be created earlier for other reasons)? If it is intentional, that 2 copies are created instead of one, then there is no solution to this, do I miss something? – Marcin L Dec 13 '15 at 20:37
  • 1
    @MarcinL: Currently, there are no memleaks. You may have a move constructor which may be cheaper than the copy. Else, you may change `sampleThread` to take by const reference and do the copy in `sampleThread` or before creating the thread (using `std::vector`...). – Jarod42 Dec 13 '15 at 20:58
  • @MarcinL : You seem to have some fundamental misunderstanding here. Why do you perceive that having two copies of an object would leak memory? – ildjarn Dec 13 '15 at 22:47
  • @ildjarn well I meant storing redundant, unused copies in the memory – Marcin L Dec 13 '15 at 23:34
  • @ildjarn take a look at the sample program in the OP, and the output, where destructor calls are also printed. Notice, that until `sampleThread` is finished, you still have one redundant `sampleClass` copy in memory, namely the one with `x=61` in this particular output. I've seen exactly the same behavior in the actual application where threads are way more sophisticated than in the example shown, and this happens also with `-O3`. It still remains non trivial conclusion to me that a straightforward passing of a class argument to a thread would result in 2 copies, instead of just one. – Marcin L Dec 14 '15 at 18:41