When is a move constructor called in practice?

Viewed 378

I've recently learned about move constructors, but a lot of the online resources don't talk about copy elision. Copy elision makes sense to me too, but it left me wondering when is the move constructor ever going to be called without a super contrived example.

From a popular SO post that explained move semantics to me https://stackoverflow.com/a/3109981/11188084

string b(x + y);                                
string c(some_function_returning_a_string());   

The post says both of these should invoke the move constructor because they take in temporaries. However, none of these actually call the move constructor (I've tested), instead they all just do copy elision, unless you force it to by explicitly writing std::move.

string b(std::move(x + y)); 
string c(std::move(some_function_returning_a_string()));

or some_function_returning_a_string returns std::move(someString). But why would you? Copy elision is even more performant than move semantics. So what are the natural cases where the move constructor would be called instead of copy elision?

Before you point me here, I felt like When Does Move Constructor get called? answer gave either contrived examples or some of them would have just done copy elision. I'm interested in learning when move constructors are called in practice.

Here's a link for the testing https://godbolt.org/z/KfczbboTr

3 Answers

In your examples the moved from objects are temporaries, but thats not always the case when moving. Sometimes we know that we can move because the moved from object will not be used anymore even though it is not a temporary. Consider this type:

struct foo {
    foo() = default;
    foo(foo&& f) noexcept {
        std::cout << "move\n";
    }
};

When you create a vector of foos and the vector reallocates it will not copy the elements, but it will move them. For example:

#include <iostream>
#include <vector>

int main() {
    std::vector<foo> v;
    v.resize(5);
    v.resize(v.capacity()+1); // force the vector to reallocate
}

output:

move
move
move
move
move

There is no way the copy or move could have been elided, because the elements are in the old place and have to get to the new place in memory somehow.

Move construction gets implicitely invoked on temporaries and RValues all the time when using wrapper types like std::tuple<>, std::pair<>, or std::variant<>. The added level of indirection prevents copy-elision from kicking in, and you end up with a call to the move constructor.

For example:

#include <tuple>
#include <iostream>

struct SomeType {
    SomeType() = default;
    SomeType(SomeType&&) {
      std::cout << "moved!\n";
    }
};

int main() {
  std::make_tuple(SomeType{}, 12, SomeType{});
}

outputs the following:

moved!
moved!

A very technical answer to the question as posted. Move constructor will be called here, and I think, the code is not contrived at all.

struct Foo
{
    Foo(std::string str) : str(std::move(str)) { }
    std::string str;
}
Related