A little bit about std::move

Luciano Almeida
8 min readDec 2, 2021

--

Move Semantics is an extremally important concept for one to understand talking about programming in c++. It is a fundamental aspect of the language that may not be that obvious and even more for one coming from another language such as Swift, C#, or Java. Understanding the various aspects of move such as l-value vs r-value types, move-only types, move constructor and assign operator and the various guidelines on how to define and use them correctly, and also in what aspects modern compilers can help, is essential for us developers to produce correct and efficient c++ code.

With that said, the goal of this article is just to go over the basics about std::move and the related aspects with some examples on how we can take advantage of it in our day-to-day code.

But before we dive deeper into move semantics there are some concepts we need to understand first: L-values, R-Values, and Universal References.

L-Values and R-Values

L-value: The concept is very simple, every expression that results in a reference for a memory location is an l-value expression. The “L” stands for left-side, meaning that it can be on the left side of an assign operator “=”. An example is a variable declaration.
In simpler terms, if we can take a reference to a memory location from the expression result, it is an l-value.

R-value: All l-values are r-values but not all r-values are l-values. If we think on the left-right side of the assign operator, an l-value can be on both left and right sides, but an r-value cannot be on the left side. An example of an r-value is a return of a function that is not a reference type.

As we can note in the example, the left-right side association is pretty straightforward. This distinction of l-value/r-value type is extremely important for move semantics as normally we can infer things like, if this is an r-value type we can probably move or compiler will generate the move implicitly depending on the context.

Universal References

The “&&” notation is known to be the syntax to r-value reference, but quoting Scott Meyers

T&& Doesn’t Always Mean “Rvalue Reference”

It means that a T&& can hold both an l-value and an r-value reference, which is why is also known as Universal Reference. But note that there are some rules to that and “&&” only means a universal reference when type deduction is involved, otherwise we can assume that it means only an r-value reference.

But for this article, the important concept to understand is that a universal reference can hold both an l-value and r-value reference and that there is the concept of perfect forwarding where a Univesal Reference can be propagated preserving the l-r valueness. We can see more details in the std::forward reference. A couple of examples:

Right, now that we know the l-value, r-value, and Universal References concepts, it is time to go back to our main topic.

std::move

So, what is exactly std::move?

From our everyday friend cppreference

std::move is used to indicate that an object t may be "moved from", i.e. allowing the efficient transfer of resources from t to another object.

Especially for large objects pass them around through our program can be very expensive because normally data have to be copied from one place to another to be stored in another part of the running program and using references or pointers are not desirable nor ideal in some situations. So to mitigate that, move is a way to efficiently transfer contents of an object to another leaving the source in a valid but undefined state.

So different from the mov instruction in an assembly that although is called “move” when you move a value from a register or memory location to another place, the value on the source register or memory location is still there, the semantics of move in c++ is, in fact, to move the data since the source doesn’t have the content after the move.

To better understand this concept let’s see an example

We can see in the example that this is a situation where move was possible because it was the last usage of the variable result (after that it would go out of scope and destructed) so adding the string value which depending on the size of the string can be expensive, using a move is much more efficient. But as we are going to talk about later we have to use it carefully because as we can see on the example after move any access of the variable is undefined behavior. Also, there is a lot we have to consider when thinking about use move such as compiler-generated implicit moves, does using explicit move can negatively affect compiler optimizations such as NRVO(Named Return Value Optimization)? Does the type has a move constructor and assign defined or implicitly generated by the compiler(because otherwise, std::move will be just a copy)?

So the advice will be, if we are unsure move is more or less efficient, we benchmark and compare the results. There are a lot of tools that can help with that!

Defining a move constructor

std::move is actually just a request to move and if the type of the object has not a move constructor/assign-operator defined or generated the move operation will fall back to a copy.

Move constructor and move assign is part of the special functions that compilers can implicitly generate, as long as some criteria are met. The compiler will be able to generate a member-wise default move construct if and only if there is no user defined copy constructor, copy assign-operator, destructor or move assign-operator and the same applies for move assign-operator.

The gist of those rules from the cppreference:

If no user-defined move constructors are provided for a class type (struct, class, or union), and all of the following is true:

* there are no user-declared copy constructors;

* there are no user-declared copy assignment operators;

* there are no user-declared move assignment operators;

* there is no user-declared destructor.

then the compiler will declare a move constructor as a non-explicit inline public member of its class with the signature T::T(T&&).

The rationale is that if the default member-wise destructor, copy constructor, copy assign-operator has to be custom, the move operators may also have to be. But if the default member-wise operator is enough for the type we can always use “default” explicit e.g. Class(Class &&) = default;

To better illustrate those rules let’s see an example:

Compiler mplicitly generated move

Just for clarification, from cpp reference

A trivially move constructible class is a class (defined with class, struct or union) that:

* uses the implicitly defined move constructor.

* has no virtual members.

* its base class and non-static data members (if any) are themselves also trivially move constructible types.

Since the second and third rules are true the only relevant for those cases is the first one which can demonstrate well the rules of implicitly generated move constructor.

Another way to see those rules being applied in our example is by checking compiler output for that code. Let’s check using clang and -ast-dump to visualize what compiler is generating:

clang -Xclang -ast-dump

We can note that for A Decl which has a user defined destructor the MoveConstructor and MoveAssignment don’t exist and the same goes for B Decl which has an user_declared copy constructor. But for C Decl, which makes into the rules of implicit generated move, we can see that both move constructor and assignment do exist and will be generated if used, because normally compiler only generates them if used in the program.

We just learned that there are some rules we should be paying attention when thinking about move operations and compilers support for those special functions.

But what if we need to define our own custom move operations?

There are some guidelines we should consider:

Use default when possible:

If we have our user-defined destructor or any other method that makes the compiler not able to generate implicit member-wise move but that will be still good enough for this case we can use default. Use of custom implemented move constructor only when absolutely necessary.

But when we do have to define a move constructor, some guidelines can be followed:

move should be always noexcept: The reason is that if a move is called on an object that does not define move constructor noexcept, trying to move this object in a function that offers a strong exception guarantee, makes it generate a copy instead to be more conservative(therefore negatively affecting performance) since it cannot know if a move can throw and then left source in an invalid state if throws happen, therefore violating that strong exception guarantee. That is why is recommended to always make move constructor and move assign-operator. Note that implicit generated and A(A&&) = default generated are noexcept.See C.66 or CppCoreGuidelines for details.

move always leave source in a valid state: It is recommended that the moved from object should be the default value of an object of that type. For example a pointer should be left as nullptr. Is the generally assumed semantics. See C.64 or CppCoreGuidelines for details.

There are a couple more core guidelines related to move in the whole document, so it is definetly worth checking that out.

Performance impact of move

When used correctly move operation can have a huge performance impact especially in contexts where we work with objects that are expensive to copy e.g. large strings, vectors or lists and even more when they are in hot paths of the application. Perf profilers can help a lot in identifying the spots, but having the understanding of move semantics can help us make good improvements. Let’s see an example:

Consider this API

But one may note that all the calls to add will result in a copy of val right? Note that a const & parameter is allowed to take an r-value argument. But here is the opportunity to improve, let’s see if when taking an r-value we can optimize this by using a move by re-designing this class

But as always, we should never just assume that something is more efficient than something else, let’s measure it!

Here is the benchmark code:

And here are the results using Clang 12.0 std=c++20 -O3 and libstdc++(GNU)

1.5x Faster

Note that the advantage of move are directly related to the size of the object so in our benchmark the larger the string the bigger the improvement.

That was all to show one simple example very similar to some issue we found in one of our library code that was a big performance win for our application and how understanding move semantics helped us to better design our API and improve our performance.

Wrapping Up

There are a lot more we could cover about move semantics and all the related topics we mentioned in this article, but the goal was to be a very basic and short overview of the concepts and try to clarify some aspects that may be not straightforward especially for someone coming from another language such as Swift. All references are linked so everything we mentioned here can be looked up in more details in those references, I do recommend all of them.

And that is it! If you got until here hope this could be helpful for you :)

References

  1. Cpp Reference std::move https://en.cppreference.com/w/cpp/utility/move
  2. Cpp Reference std::forward https://en.cppreference.com/w/cpp/utility/forward
  3. Cpp Reference is_trivially_move_constructible: https://www.cplusplus.com/reference/type_traits/is_trivially_move_constructible/
  4. Universal References: https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers
  5. C++ Rvalue References Explained: http://thbecker.net/articles/rvalue_references/section_01.html
  6. CppCoreGuidelines.md

--

--

Luciano Almeida

Aspiring Compiler Engineer, Swift and OpenSource enthusiast