When is a move constructor called in practice?
Asked Answered
C

3

5

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://mcmap.net/q/15702/-what-is-move-semantics

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

Caput answered 1/6, 2021 at 14:48 Comment(5)
"(I've tested)" Can you elaborate on how you tested, please? Maybe the issue is just your methodology.Iridissa
I'd say the majority of the time, you'll get a move instead of copy elision. One example is a growing std::vector. If you exceed capacity it has to allocate new element objects and move the existing ones in there.Carillonneur
Note that C++17 changed rules about copy ellision, it is now mandated in many cases (previously you could possibly disable it by meddling with compiler options). Even before C++17, compilers often optimized the move away, for example when RVO (Return Value Optimization) could be used.Laden
"I felt like ... answer gave either contrived examples or some of them would have just done copy elision." You are correct that some of the examples now guarantee copy elision. I suspect that you are asking to be convinced that the other examples are not contrived.Stover
void Foo(std::string&& s); may involve a move constructor, or may use a move assignment, or may not move at all — depends on what Foo's implementation does. void Bar(std::string s) called through std::string s = "Hello"; Bar(std::move(s)); will use a move constructor.Tremaine
U
6

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.

Uncork answered 1/6, 2021 at 15:5 Comment(7)
Technically the first resize could have allocated enough space for (at least) 12 foos and then there'd be no movingAllege
@Allege true but that does not change the fact that as soon as I request space for more than the inital capacity the elements will be movedUncork
Maybe v.resize(v.capacity() + 1); instead of r.resize(12); would work around that possibility. I believe that would be required to reallocate in all cases, guaranteeing the move happens.Fara
@FrançoisAndrieux nice, I'll fix thatUncork
@463035818_is_not_a_number This is a really good example! I haven't thought about non-temporaries being moved. Are there any examples using temporaries? It's funny how all the move semantic tutorials talk about temporaries, but they seem to always be copy elided in reality.Caput
@JoshuaSegal sorry, no. I am not the best person to ask this. Rules for eliding have changed over time while I am still focused on C++11. Though, you just need to find a case that does not allow elision here en.cppreference.com/w/cpp/language/copy_elision.Uncork
@JoshuaSegal My answer covers exactly the kind of temporaries example you are asking about.Iridissa
I
2

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!
Iridissa answered 1/6, 2021 at 15:1 Comment(0)
H
0

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;
}
Hothead answered 1/6, 2021 at 14:54 Comment(5)
I think OP was specifically looking for examples that don't require an explicit std::move().Swivel
Yeah, this example seems quite contrived to me. Like why would declare a variable just to move it?Caput
@JonReeves OP never mentioned that in the question.Hothead
@JoshuaSegal ok, I was trying to show something which is easier to grasp. I will edit to include real working example I have used many times.Hothead
@JoshuaSegal added.Hothead

© 2022 - 2024 — McMap. All rights reserved.