7

The following code compiles without warnings in Visual Studio 2019 msvc x64:

class ThreadRunner {
public:
    void start() {
        m_thread = std::move(std::thread(&ThreadRunner::runInThread, this));
    }
    
private:
    void runInThread() {
        for (int i = 0; i < 1000 * 1000; i++) {
            std::cout << "i: " << i << "\n";
        }
    };
    std::thread m_thread;
};

However if I compile the same code with x64-Clang I get the following warning:

warning : moving a temporary object prevents copy elision [-Wpessimizing-move]

Does this mean that I should have written:

m_thread = std::thread(&ThreadRunner::runInThread, this);

instead? And the compiler would have optimized away ("copy elided") the temporary variable?

Will msvc x64 also copy elide the temporary variable?

I did an experiment:

struct B {
    void f1() {
        a = A(5);
    }
    void f2() {
        A tmp = A(5);
        a = tmp;
    }
    void f3() {
        a = std::move(A(5));
    }
    void f4() {
        A tmp = A(5); 
        a = std::move(tmp);  
    }
    A a;
};

f1, f3 and f4 produce the same sequence of calls to A member functions:

default ctor
ctor
move = operator

f2 produce another result:

default ctor
ctor
= operator
Andy
  • 3,251
  • 4
  • 32
  • 53
  • 1
    Since this is assignment I don't think it's possible for copy elision to take place, but as eerorika points out your `std::move` is redundant because you're casting an rvalue into an rvalue, so the clang warning is still pertinent. – Nathan Pierson Jan 18 '22 at 08:14
  • 1
    Are you ever going to instantiate a `ThreadRunner`, do other stuff, then call `start`? I'd initialise `m_thread` with those arguments – Caleth Jan 18 '22 at 09:32
  • @Caleth: Thank you. Say for instance that a ThreadRunner is a member variable of a MyWidget class. When I press a "Run" button the ThreadRunner is started. I could have made a std::unique_ptr a member variable of a MyWidget class instead, and started the thread in ThreadRunner constructor as you suggest. But how is that better? – Andy Jan 18 '22 at 09:38
  • 1
    [OT] `1000 * 1000` can be written `1'000'000` since C++14. – Jarod42 Jan 18 '22 at 09:39

2 Answers2

8

Does this mean that I should have written:

m_thread = std::thread(&ThreadRunner::runInThread, this);

instead?

Yes, it means that and you should have. std::thread(&ThreadRunner::runInThread, this) is a prvalue, and you gain nothing by casting it to an xvalue.

eerorika
  • 232,697
  • 12
  • 197
  • 326
4

The two versions

m_thread = std::thread(&ThreadRunner::runInThread, this);

and

m_thread = std::move(std::thread(&ThreadRunner::runInThread, this));

behave identically. No elision is possible in either case, since this is assignment to, not initialization of, m_thread. The temporary object must be constructed and then there will be a move assignment from it in either version.

The hint is still correct though, since std::move on a temporary either doesn't have any effect at all (as here) or prevents elision if used in a context where copy elision would otherwise be allowed/mandatory, for example if this was the initializer of m_thread instead of an assignment to it.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • My experiment above shows that you are correct in that the two versions behave identically, at least since C++14. Probably since C++11? And since this is assignment to, not initialization this is not called copy elision: OK. But what is it called? Since move = operator was introduced in C++11 this optimization did not exist before C++11? So it must be specified somewhere in the C++11 standard? – Andy Jan 18 '22 at 09:25
  • 1
    @Andy It is called a move assignment. It calls the move assignment operator (`operator=(thread&&)`) instead of the copy assignment (`operator=(const thread&)`). But it calls that with or without `std::move` because `std::thread(&ThreadRunner::runInThread, this)` is a rvalue expression already. `std::thread`'s copy assignment is deleted anyway, it cannot call any other assignment operator. In general, the move assignment version is allowed to modify the passed object and can therefore be more efficient than the copy assignment by e.g. reusing storage the passed object allocated. – user17732522 Jan 18 '22 at 09:28
  • @Andy See [What is move semantics?](https://stackoverflow.com/questions/3106110/what-is-move-semantics). – user17732522 Jan 18 '22 at 09:30
  • 2
    @Andy And it isn't an optimization. Move assignment is simply a language construct that didn't exist pre-C++11. The assignment would have always called the copy assignment operator pre-C++11. But as I mentioned copying `std::thread` doesn't make any sense (how do you copy a thread?). `std::thread` didn't exist pre-C++11 either. – user17732522 Jan 18 '22 at 09:33