Blog Post

When C++ Doesn't Move

Illustration: When C++ Doesn't Move
Information

This article was first published in July 2019 and was updated in July 2024.

Move semantics were introduced in C++11 with the hope of adding more performance to an already performant language. In most cases, move semantics were successful in achieving efficiency, but there are some equally confusing and important situations where a move won’t be performed, and as a C++ developer, it’s beneficial to understand when these situations could occur.

Today, move semantics are relatively unchanged in modern revisions of C++, with the only minor modifications coming in C++20 relating to r-value references, and return values.

But before you start reading about the pitfalls and performance gains surrounding move semantics, I’d suggest first reading up on r-value references and move semantics and coming back to this post with a better base of knowledge to build upon.

When a Move Is Not Performed

Sometimes the compiler cannot call the move constructor and it defaults back to the copy constructor or another operation — even when std::move is called. From the compiler output, you have no way of knowing when this happens, and in most instances, you’ll be losing performance without even realizing it (or at least throwing away that slight performance increase you could benefit from).

For here on out, I’ll be using a class that will print the information from each constructor or assignment. It’ll highlight the constructors and assignments that are called, in addition to sharing at which point during the execution of the following examples they’re called:

class Lifetime {
public:
  Lifetime() { std::cout << "Constructor" << std::endl; }
  Lifetime(const Lifetime &other) {
    std::cout << "Copy Constructor" << std::endl;
  }
  Lifetime(Lifetime &&other) noexcept {
    std::cout << "Move Constructor" << std::endl;
  }
  Lifetime &operator=(const Lifetime &other) {
    std::cout << "Copy Assignment" << std::endl;
    return *this = Lifetime(other);
  }
  Lifetime &operator=(Lifetime &&other) noexcept {
    std::cout << "Move Assignment" << std::endl;
    return *this;
  }
};

The Move Is Implicitly Deleted

The compiler will refuse to implicitly declare the move constructor/assignment in some cases. We can understand this better when reading N3225 section 12.8/10:

If the definition of a class X does not explicitly declare a move constructor, one will be implicitly declared as defaulted if and only if

  • X does not have a user-declared copy constructor,
  • X does not have a user-declared copy assignment operator,
  • X does not have a user-declared move assignment operator,
  • X does not have a user-declared destructor, and
  • the move constructor would not be implicitly defined as deleted.

The rule of zero advises us not to declare any default operations, and this advice holds true if we want to use move semantics. When any of the copy semantics or the destructor are explicitly defined and move semantics are not explicitly defined, the compiler will not implicitly define any move semantics. What this means is the class isn’t movable and it’ll revert to a copy:

class Lifetime {
public:
  Lifetime() { std::cout << "Constructor" << std::endl; }
  Lifetime(const Lifetime &other) {
    std::cout << "Copy Constructor" << std::endl;
  }
};

int main() {
  Lifetime temp;
  Lifetime x = std::move(temp);
  return 0;
}

Output:

Constructor
Copy Constructor

We can see this when we only define the copy constructor in the Lifetime class and attempt a move: The output will show a copy is actually made. The explanation of this lies within the C++ standard passage seen above, with the relevant line repeated below, where X is Lifetime:

X does not have a user-declared copy constructor

If we use the original Lifetime class declared at the top of the post, we’ll see a move operation:

Constructor
Move Constructor

Moving a const Value

Constness will often disable move operations, and you can see this from the definition of the move constructor: It takes in a non-const r-value reference. When a const value is passed to std::move, the compiler will revert to a copy:

int main() {
  const Lifetime temp;
  Lifetime x = std::move(temp);
  return 0;
}
Constructor
Copy Constructor

However, if you have some mutable data in your const object, it can make sense to define a const move constructor:

class Lifetime {
public:

 ...

  Lifetime(const Lifetime &&other) noexcept : value(other.value) {
    std::cout << "Const Move Constructor" << std::endl;
    other.value = 0;
  }

  mutable int value;
};

Now the const move constructor will be called.

Strictly speaking, to fulfill the requirements of a move, no mutable members should be defined. However, because we’ve been explicit in defining value as mutable, it theoretically makes sense to allow a const move constructor. That said, I haven’t found any reason for this as of yet, so it’s good to know that const data won’t be moved:

Constructor
Const Move Constructor

Move Operations and the STL (Standard Template Library)

If your code is making use of the STL (which it should), it’s a good idea to make sure your classes will play nicely.

Because the STL can still be used with C++ exceptions disabled, it’s important to notify the compiler when a move constructor or move assignment won’t throw, i.e. when it’s noexcept. When we forget to do this, some operations of the STL will fail to use move semantics — for example, the resize method of vector.

If we remove noexcept from our Lifetime move constructor:

Lifetime(Lifetime &&other) {
  std::cout << "Move Constructor" << std::endl;
}

And then attempt a resize of a vector of Lifetimes, then we’ll see the copy constructor is called:

int main() {
  std::vector<Lifetime> temp;
  temp.emplace_back();
  temp.emplace_back();

  temp.resize(1);
  return 0;
}

Output:

Constructor
Constructor
Copy Constructor

To avoid this, we always want to ensure we declare noexcept when we’re sure the move constructor or assignment won’t throw.

RVO Has Been Used

Now, this is a good one. RVO, or return value optimization, is an optimization the compiler is allowed to use to merge the copy or move. It’ll construct the object in the place of the assigned value. RVO is the one time where we don’t want to see a move operation, as it’s more efficient than a move operation:

Lifetime getLifetime() {
  Lifetime lifetime;
  return lifetime;
}

int main() {
  const auto localLifetime = getLifetime();
  return 0;
}

Output:

Constructor

Now, if we ignored RVO, in the example above we’d expect to see both a constructor being called and a move or copy constructor placing the value into localLifetime. But RVO can perform its optimizations and construct a Lifetime object into localLifetime directly.

It’s important to note that this isn’t always the case, as RVO is up to the discretion of the compiler. However, most modern compilers will ensure RVO is used.

FAQ

Here are a few frequently asked questions about move semantics.

What Are Move Semantics in C++?

Move semantics in C++ allow the efficient transfer of resources from one object to another, avoiding the overhead of making a copy. This is particularly useful for objects that manage resources, which are expensive to duplicate.

When Are Move Semantics Used?

Move semantics may be utilized in C++ when returning objects from functions, passing objects by value, manipulating dynamic memory (such as resizing an std::vector or std::string), and when handing over unique ownership (as with unique_ptr).

How Do I Implement a Move Constructor in C++?

A move constructor enables transfer of resource ownership and is typically implemented using an rvalue reference (designated with &&). However, C++ may implicitly generate a move constructor if it isn’t explicitly defined in certain situations.

What Is the Purpose of std::move?

std::move() casts an lvalue to an rvalue, enabling efficient resource transfer via move constructors or move-assignment operators, without triggering a deep copy. But be careful with it, as moving from an object leaves it in a valid but unspecified state.

How Do C++ Move Semantics Improve Performance?

Move semantics may improve performance by eliminating unnecessary deep copies of objects, which can be costly in terms of time and memory. By transferring resources directly, move semantics reduce the overhead associated with managing dynamic resources, leading to more efficient code execution.

Conclusion

In this post, we learned that C++ move semantics may be deleted, constness will prohibit move operations, noexpect is required for move semantics when using the STL, and RVO may be used instead of moving. It’s important to keep these points in mind when assuming that a move will be performed, as std::move doesn’t always mean a move will be performed.

Here at PSPDFKit, we’ve implemented clang tidy as one of our many CI jobs, and it has many checks to ensure you’re using move semantics correctly or optimizing when possible. It’s a great tool to pick up the slack when you forget rules at odd times.

Author
Nick Winder Core Engineer

When Nick started tinkering with guitar effects pedals, he didn’t realize it’d take him all the way to a career in software. He has worked on products that communicate with space, blast Metallica to packed stadiums, and enable millions to use documents through PSPDFKit, but in his personal life, he enjoys the simplicity of running in the mountains.

Share Post
Free 60-Day Trial Try PSPDFKit in your app today.
Free Trial

Related Articles

Explore more
DEVELOPMENT  |  Web • C++

My Experience with Web Development from a Systems Programming Perspective

BLOG  |  Web • C++ • WebAssembly

Render Performance Improvements in PSPDFKit for Web

DEVELOPMENT  |  C++ • Office • Tips

Testing Subjective Office Conversion Results