Logo

Blog


Why you should use std::move only rarely

In today's post, I try to tackle a topic that comes up frequently in my classes, move semantics, and when to use std::move. I will explain to you why not say std::move yourself (in most cases).

As already said, move semantics is a topic that comes up frequently in my classes, especially the part when to use std::move. However, move semantics is way bigger than what today's post covers, so don't expect a full guide to move semantics.

The example below is the code I used to make my point: don't use std::move on temporaries! Plus, in general, trust the compiler and use std::move only rarely. For this post, let's focus on the example code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class S {
public:
  S() { printf("default constructor\n"); }
  ~S() { printf("deconstructor\n"); }

  A Copy constructor
  S(const S&) { printf("copy constructor\n"); }

  B Move constructor
  S(S&&) { printf("move constructor\n"); }
};

void Use()
{
  S obj{
    S{}  C Creating obj with a temporary of S
  };
}

Here we see a, well, perfectly moveable class. I left the assignment operations out. They are not relevant. Aside from the constructor and destructor, we see in A the copy constructor and in B the move constructor. All special members print a message to identify them when they are called.

Further down in Use, we see C, a temporary object of S used to initialize obj, also of type S. This is the typical situation where move semantics excels over a copy (assuming the class in question has moveable members). The output I expect, and I wanted to show my participants, is:

1
2
3
4
default constructor
move constructor
deconstructor
deconstructor

However, the resulting output was:

1
2
default constructor
deconstructor

Performance-wise the output doesn't look bad, but it doesn't show a move construction. The question is, what is going on here?

This is the time to apply std::move, right?

At this point, somebody suggestion was to add std::move:

1
2
3
4
5
6
void Use()
{
  S obj{
    std::move(S{})  C Moving the temporary into obj
  };
}

This change indeed leads to the desired output:

1
2
3
4
default constructor
move constructor
deconstructor
deconstructor

It looks like we just found proof that std::move is required all the time. The opposite is the case! std::move makes things worse here. To understand why, let's first talk about the C++ standard I used to compile this code.

Wait a moment!

In C++14, the output is what I showed you for both Clang and GCC. Even if we compile with -O0 that doesn't change a thing. We need the std::move to see that the move constructor is called. The key here is that the compiler can optimize the temporary away, resulting in only a single default construction. We shouldn't see a move here because the compiler is already able to optimize it away. The best move operation will not help us here. Nothing is better than eliding a certain step. Eliding is the keyword here. To see what is going on, we need to use the -fno-elide-constructors flag, which Clang and GCC support.

Now the output changes. Running the initial code, without the std::move in C++14 mode shows the expected output:

1
2
3
4
default constructor
move constructor
deconstructor
deconstructor

If we now switch to C++17 as the standard, the output is once again:

1
2
default constructor
deconstructor

Due to the mandatory copy elision in C++17, even with -fno-elide-constructors, the compiler must now elide this nonsense construction. However, if we apply std::move to the temporary copy elision doesn't apply anymore, and we're back in seeing a move construction.

You can verify this on Compiler Explorer godbolt.org/z/G1ebj9Yjj

The take away

That means, hands-off! Don't move temporary objects! The compiler does better without us.

Andreas