Welcome to the next pikoTutorial!
std::move
moves an object
One of the most prevalent misconceptions is that std::move
actually moves an object. In reality, std::move
does not perform any movement – it casts its argument to an rvalue reference. This enables the compiler to apply move semantics, but std::move
itself does not move anything. The actual move operation is performed by the move constructor or move assignment operator of the object. In simple words – if the move constructor of an object does not execute any movement of its internals, std::move
won’t change that fact.
After std::move
object becomes empty or invalid
Another common misconception is that objects passed to std::move
are always left in an invalid state. While it’s true for some objects like smart pointers which are nullified, there are many types which are still valid after moving. For example, an int
is a primitive type, it doesn’t have a constructor or destructor, so it still holds the value even after moving:
int value = 12;
int moved_value = std::move(value);
std::cout << moved_value << std::endl; // shows 12
std::cout << value << std::endl; // original variable still holds 12
Also in case of complex types which don’t specify any move-specific behavior, you can expect the original object to preserve its state. For example, if I create a simple struct Point
and don’t implement a custom move constructor, the compiler-generate move constructor won’t include any modification of the “moved-from” object:
struct Point
{
int x, y;
};
int main()
{
Point point {12, 24};
Point moved_point = std::move(point);
std::cout << moved_point.x << ", " << moved_point.y << std::endl; // prints "12, 24"
std::cout << point.x << ", " << point.y << std::endl; // still prints "12, 24"
}
std::move
is required to trigger move semantics
Some developers mistakenly believe that std::move
is always necessary to avoid copying objects. While std::move
is crucial for explicitly invoking move semantics, compilers are often able to elide copies and perform moves implicitly, thanks to Return Value Optimization (RVO) and Named Return Value Optimization (NRVO). In many cases, relying on the compiler’s optimizations can result in cleaner and more efficient code.
std::string CreateString() {
std::string local = "Hello, World!";
return local; // RVO makes std::move unnecessary here
}
std::move
can force non-movable types to be moved
I heard once that you can use std::move
on a non-movable type to make it movable . This is not true because std::move
is not responsible for the actual moving logic. Move semantics only apply to types that have move constructors and move assignment operators implemented.
class NonMovable
{
public:
NonMovable() {}
NonMovable(NonMoveable&&) = delete; // deleted move constructor
};
int main()
{
NonMovable obj1;
NonMovable obj2(std::move(obj1)); // compilation error
}
std::move
triggers move semantics always
When std::move
is applied to a const object, it casts the object to an rvalue reference, but since the object is const, its move constructor cannot be invoked. Instead, the copy constructor is used to create a new object. This is because move constructors typically modify the source object, which is not allowed for const objects. Thus, attempting to move a const object results in a copy operation, not a move. Understanding this nuance is crucial for correctly applying move semantics and avoiding unintended performance hits.
#include <iostream>
class SomeClass
{
public:
SomeClass() {}
SomeClass(const SomeClass&) { std::cout << "Copy constructor" << std::endl; }
SomeClass(SomeClass&&) {std::cout << "Move constructor" << std::endl; }
};
int main()
{
const SomeClass obj1;
SomeClass obj2(std::move(obj1)); // prints "Copy constructor"
}