Move Semantics
See why C++ lets a program transfer a resource out of one object into another instead of copying it, and how rvalue references, std::move, and copy elision work together to make that efficient.
Move semantics lets a program transfer ownership of a resource from a source object that is about to be destroyed or discarded into a destination object, instead of duplicating that resource the way a copy would. For a class that owns something expensive to duplicate, such as dynamically allocated memory, moving can be dramatically cheaper than copying, because it only needs to repoint a handful of member values instead of allocating new storage and copying every element.
Why Move Semantics Exists
Before C++11, initializing or assigning an object from a temporary always meant copying, even though the temporary was about to be destroyed anyway and its resources could safely be taken over instead of duplicated. Move semantics, introduced in C++11, lets a class define cheaper behavior specifically for that situation — transferring a resource out of a source that no longer needs it, rather than duplicating it.
The Cost of Copying
Consider a class that owns a dynamically allocated array, with a deep-copying copy constructor as covered in the Copy Semantics topic. A deep copy allocates new storage and copies every element — a cost proportional to the resource's size. When the source object is a temporary that's about to be discarded anyway, that cost is wasted, because nothing needs the original once the copy exists.
Without move support, initializing result could require a full deep copy of a 1000-element array purely to transfer data out of an object that is about to disappear when makeBuffer returns.
class Buffer
{
public:
explicit Buffer(int sizeValue) : size(sizeValue), data(new int[sizeValue]) {}
int size;
int* data;
};
Buffer makeBuffer()
{
Buffer b(1000);
return b;
}
Buffer result = makeBuffer();Copy vs Move: Conceptual Difference
Copying preserves the source; moving deliberately empties it in exchange for avoiding duplication.
Aspect | Copy | Move
What happens to the resource | Duplicated: source and destination | Transferred: destination takes it over,
| each own a separate resource | source gives it up
Typical cost | Proportional to resource size | Constant-time: repoint a few members
State of source afterward | Unchanged | Left in a valid but unspecified state
When it's appropriate | Source is still needed afterward | Source is a temporary, or explicitly given upLvalues and Rvalues at the Conceptual Level
At a conceptual, practical level, an lvalue is an expression that refers to an object with identity that is generally expected to be usable again afterward, such as a named variable. An rvalue is an expression whose result is a temporary value not expected to be reused afterward, such as the result of Buffer(100) or the return value of a function returning by value. This is a simplified framing; the full standard rules define several value categories (lvalue, prvalue, xvalue, and others), but this level is enough to understand how move semantics selects between copying and moving.
x, used again in the next line, is an lvalue expression. x + 1 is an rvalue expression: a temporary result with no name of its own.
int x = 5;
int y = x + 1;Rvalue References
An rvalue reference, written with &&, is a reference type that can bind to an rvalue (a temporary). It exists so that a function — most importantly a move constructor or move assignment operator — can be overloaded differently depending on whether its argument is a temporary, safe to take resources from, or a named object that might still be needed, which requires an explicit request before its resources can be taken.
When both overloads exist, a temporary Buffer preferentially selects the Buffer&& overload; a named Buffer object selects the const Buffer& overload instead.
void handle(const Buffer& b) // binds to lvalues and rvalues alike, but only reads
{
}
void handle(Buffer&& b) // binds only to rvalues: temporaries
{
}The Move Constructor
A move constructor initializes a new object by transferring the resource out of an existing object of the same type, rather than copying it. It takes its parameter as an rvalue reference to the class: ClassName(ClassName&& other).
Instead of allocating a new array and copying elements, the move constructor takes other's pointer and size directly, then sets other's own members to a state where its destructor will not delete anything. Ownership of the underlying array has been transferred, not duplicated.
class Buffer
{
public:
explicit Buffer(int sizeValue) : size(sizeValue), data(new int[sizeValue]) {}
Buffer(Buffer&& other) noexcept
: size(other.size), data(other.data)
{
other.size = 0;
other.data = nullptr;
}
~Buffer()
{
delete[] data;
}
int size;
int* data;
};Move Constructor Syntax
A move constructor's parameter is written ClassName&& other, and it is almost always marked noexcept when it genuinely cannot throw. This matters because it is what allows standard-library containers to use a move instead of falling back to a copy in situations such as reallocation, though the mechanics of that container decision belong to the standard library's own topics.
The Move Assignment Operator
A move assignment operator transfers a resource into an already-existing object, releasing whatever resource that object previously held.
The current object's own resource is released first, then the source's resource is taken over, and finally the source is left in a state its destructor can safely run against.
Buffer& operator=(Buffer&& other) noexcept
{
if (this == &other)
{
return *this;
}
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
return *this;
}Move Assignment Syntax
A move assignment operator has the signature ClassName& operator=(ClassName&& other) noexcept. Like copy assignment, it returns *this by reference, and it typically guards against an object being moved into itself — unusual, but possible through aliasing — so as not to corrupt the object.
std::move
std::move is a standard-library function template that converts its argument into an rvalue reference, making it eligible to bind to && overloads such as the move constructor and move assignment operator, even though the argument is a named object — an lvalue — that would not otherwise select those overloads.
Without std::move(a), the expression a is an lvalue, and initializing b from it would select the copy constructor. std::move(a) casts a to an rvalue reference, so overload resolution selects Buffer(Buffer&&) instead.
Buffer a(100);
Buffer b(std::move(a)); // selects the move constructorWhat std::move Actually Does
Despite its name, std::move does not itself move anything. It is primarily a cast — it changes how the compiler classifies the expression, which affects overload resolution. The actual transfer of a resource happens only if that cast expression is then used to select a move constructor or move assignment operator that has been written to perform the transfer; the selected move operation is what performs the transfer, not std::move itself. Passing the result of std::move to a type with no move constructor simply falls back to selecting the copy constructor, and nothing is transferred at all.
std::move(a) still produces an rvalue reference, but since NoMove declares no move constructor, overload resolution falls back to the copy constructor. No transfer happens; a full copy is made instead.
class NoMove
{
public:
NoMove() = default;
NoMove(const NoMove&) { /* only a copy constructor exists */ }
};
NoMove a;
NoMove b(std::move(a)); // still copies: NoMove has no move constructor to selectMoved-From Objects
After an object has been used as the source of a move — its resources transferred out — it is called a moved-from object. The object still exists; it has not been destroyed, but its state has typically changed, since any resource it owned has been given away.
a is still a valid Buffer object — it can still be destroyed safely, or reassigned — but it no longer owns the array; b does.
Buffer a(100);
Buffer b(std::move(a));
// a.data is nullptr and a.size is 0 here, because that is how this class's move constructor was writtenValid but Unspecified Moved-From State
The C++ standard library's own types, such as std::string and std::vector, guarantee that after being moved from, an object is left in a valid but unspecified state: it is safe to use in any way that does not depend on its specific value, such as being destroyed, reassigned, or having member functions like size() or empty() called, but the program must not assume what value it currently holds. A user-written class's move operations can choose to leave a more specific, documented state, as the Buffer example above does by resetting to nullptr and 0 — but that is a decision the class author makes, not something the language provides automatically for every class.
s remains a fully usable std::string object after being moved from — it simply should not be read for a specific expected value until it is given a new one.
std::string s = "hello";
std::string t = std::move(s);
// s is now valid but unspecified: safe to reassign or destroy, but its contents should not be relied on
s = "reassigned"; // OK: this is a perfectly valid use of a moved-from objectResource Transfer
A hand-written move operation must, in general: take over the source's resource-owning members directly (copying the pointer or handle itself, not what it refers to), then leave the source in a state its destructor can safely run against, typically by nulling out any owning pointer so the source's destructor deletes nothing.
label is itself a std::string, which already has its own move constructor, so std::move(other.label) transfers it efficiently. data, a raw owning pointer, is taken over directly and other.data is nulled out so its destructor does not delete the array Widget now owns.
class Widget
{
public:
Widget(std::string labelValue, int sizeValue)
: label(labelValue), size(sizeValue), data(new int[sizeValue])
{
}
Widget(Widget&& other) noexcept
: label(std::move(other.label)), size(other.size), data(other.data)
{
other.size = 0;
other.data = nullptr;
}
~Widget()
{
delete[] data;
}
std::string label;
int size;
int* data;
};Efficiency Gained From Moving
Moving a Buffer of any size involves copying a fixed, small number of member values — a pointer and an int. Copying the same Buffer requires an allocation and copying every element it holds. For large resources, this difference is the entire reason move semantics exists: it turns an operation whose cost grows with the resource's size into one whose cost does not, though the class must be written correctly to realize that benefit.
When Moves Happen Automatically
A move constructor or move assignment operator is selected automatically, without writing std::move explicitly, whenever the source expression is already an rvalue — for example, a temporary object, the result of a function returning by value in cases where copy elision does not apply first, or an expression explicitly cast with std::move.
Buffer(100) is already an rvalue, so no explicit std::move is needed for it to be eligible for a move (or, as covered next, to be elided entirely).
Buffer makeBuffer()
{
return Buffer(100); // Buffer(100) is a temporary: an rvalue
}
Buffer b = makeBuffer(); // may move (or be elided) -- no std::move needed hereCopy Elision and RVO/NRVO
In many cases, the compiler is permitted — and in specific cases required by the C++17 standard — to skip the copy or move entirely and construct the object directly in its final location, a technique called copy elision. Since C++17, initializing an object directly from a prvalue, such as Buffer(100) in return Buffer(100);, is guaranteed to be elided: no copy or move constructor is called at all, not even an inaccessible one. Named return value optimization (NRVO) — eliding the copy or move when a function returns a named local object — is a related but separate optimization that compilers are permitted, but not required, to perform.
makeBuffer's return is guaranteed to avoid any copy or move constructor call under C++17 rules. makeNamedBuffer's return is a common candidate for NRVO, but a program must not rely on it happening — the class must still have a working move (or copy) constructor available for cases where the compiler does not elide.
Buffer makeBuffer()
{
return Buffer(100); // guaranteed elision since C++17: built directly in the caller's storage
}
Buffer makeNamedBuffer()
{
Buffer local(100);
return local; // NRVO: commonly elided, but not guaranteed by the standard
}Return-by-Value and Move
Because of copy elision and move semantics together, returning a resource-owning object by value from a function is efficient in modern C++ — there is usually no need to return a pointer or use an out-parameter merely to avoid a copy. When elision does not apply, such as returning a named local object in a case the compiler is not able to elide, the move constructor is selected automatically over the copy constructor, because a named local object about to be returned is treated as an rvalue for that specific purpose.
If NRVO does not apply here, the compiler still avoids a full deep copy by selecting Buffer's move constructor instead, because local is about to go out of scope regardless.
Buffer makeBuffer()
{
Buffer local(100);
// ... code that uses local ...
return local; // treated as an rvalue for overload resolution if not elided: prefers the move constructor
}Moving Named Objects Requires std::move
Outside of a return statement's special treatment, a named object is always an lvalue, so passing it to a function or using it to initialize another object selects the copy constructor by default, even if that named object will never be used again afterward. std::move must be used explicitly to opt into moving from a named object in every other context.
The compiler cannot tell, just from looking at this code, that a will never be used again after the first call; std::move is the program's explicit statement that it is safe to take a's resources.
Buffer a(100);
processAndDiscard(a); // copies: a is an lvalue here, even though it's unused afterward
processAndDiscard(std::move(a)); // moves: explicitly requests treating a as an rvalueBest Practices
1. Mark move constructors and move assignment operators noexcept whenever they genuinely cannot throw — this is what allows standard containers to use moves instead of falling back to copies in situations like reallocation.
2. Always leave a moved-from object's owning members in a state its own destructor can safely process, typically by nulling out an owning pointer, rather than leaving stale values behind.
3. Use std::move only on an object you are finished with in its current form — after moving from it, treat its value as unspecified rather than relying on any particular content, unless the class's own documentation guarantees otherwise.
4. Don't write return std::move(local); — doing so can actually prevent copy elision or NRVO from applying, and the compiler already treats a returned local object as movable in the cases where elision does not happen.
5. Guard a hand-written move assignment operator against moving an object into itself, just as with copy assignment.
6. Prefer relying on standard-library types' own correct move behavior over hand-writing move operations, unless the class directly owns a raw resource.
Complete Example
b is copy-constructed from a, leaving a untouched. c is move-constructed from a via std::move, leaving a in the moved-from state its move constructor defines (size 0, data nullptr).
a is constructed with size 5 b(a) copies a, since a is an lvalue: a is unaffected c(std::move(a)) moves from a, since std::move casts it to an rvalue: a.size becomes 0 and a.data becomes nullptr the final line reads a.size as 0, b.size as 5, and c.size as 5 The program's output.
#include <iostream>
class Buffer
{
public:
explicit Buffer(int sizeValue)
: size(sizeValue), data(new int[sizeValue])
{
std::cout << "Constructed size " << size << '\n';
}
Buffer(const Buffer& other)
: size(other.size), data(new int[other.size])
{
for (int i = 0; i < size; ++i)
{
data[i] = other.data[i];
}
std::cout << "Copied size " << size << '\n';
}
Buffer(Buffer&& other) noexcept
: size(other.size), data(other.data)
{
other.size = 0;
other.data = nullptr;
std::cout << "Moved\n";
}
~Buffer()
{
delete[] data;
}
int size;
int* data;
};
int main()
{
Buffer a(5);
Buffer b(a); // copy constructor: a is an lvalue
Buffer c(std::move(a)); // move constructor: explicitly cast to an rvalue
std::cout << "a.size=" << a.size << " b.size=" << b.size << " c.size=" << c.size;
return 0;
}Constructed size 5
Copied size 5
Moved
a.size=0 b.size=5 c.size=5Mental Model
Picture a move as handing over the same box rather than packing a new one.
No new array is allocated and no elements are copied — only the pointer itself changes hands, from a to b.
Before move:
a.data ---> [ 10 | 20 | 30 ]
b.data ---> (not yet constructed)
After Buffer b(std::move(a)):
a.data ---> nullptr (a gave up ownership)
b.data ---> [ 10 | 20 | 30 ] (b now owns the same array a used to own)Quick Reference
A quick-reference summary of the move-semantics mechanics covered in this topic.
Concept | Example | Meaning
Rvalue reference | Buffer&& other | Binds only to temporaries / std::move'd expressions
Move constructor | Buffer(Buffer&& other) noexcept | Transfers a resource into a new object
Move assignment operator | Buffer& operator=(Buffer&& other) | Transfers a resource into an existing object
std::move | std::move(a) | Casts a to an rvalue reference; does not itself move
Moved-from state | a.data == nullptr (this class) | Valid but unspecified in general (library types)
Copy elision (C++17, prvalue) | return Buffer(100); | Guaranteed: no copy/move constructor called
NRVO (named local return) | return local; | Permitted, not guaranteedKey Takeaways
Move semantics transfers a resource out of a source object instead of duplicating it, which is typically far cheaper than copying for classes that own large resources.
An rvalue reference (&&) binds to temporaries and to expressions explicitly cast with std::move, allowing move constructors and move assignment operators to be selected via overload resolution.
std::move does not itself move anything — it is a cast to an rvalue reference; the actual transfer happens only if a move constructor or move assignment operator is then selected and performs it.
A moved-from object remains valid but its value should not be relied upon; standard-library types guarantee a valid but unspecified state, while user-written classes may document a more specific one.
A named object is always an lvalue and requires an explicit std::move to be moved from, except for a returned local object, which the compiler treats as movable automatically when copy elision does not apply.
C++17 guarantees copy elision when initializing directly from a prvalue; NRVO for a named local return is permitted but not guaranteed by the standard.
What is the conceptual difference between copying an object and moving it, in terms of what happens to the source object's resource?
Copying duplicates the resource: the source keeps its own resource unchanged, and the destination gets an independent copy. Moving transfers the resource: the destination takes over the resource the source held, and the source is left without it, in a valid but unspecified (or class-documented) state, rather than being duplicated.
What does std::move(a) actually do, and why doesn't it, by itself, move anything?
std::move(a) is essentially a cast: it converts the expression a into an rvalue reference, which changes how the compiler classifies it for the purposes of overload resolution. It performs no transfer of data on its own. The actual transfer only happens if that cast expression is subsequently used to select a move constructor or move assignment operator that has been written to perform the transfer — passing the result to a type with no move constructor simply falls back to copying instead.
Given Buffer a(100); processAndDiscard(a); where processAndDiscard takes a Buffer&& parameter, why does this fail to compile (or fail to select the intended overload) without wrapping a in std::move?
a is a named variable, which makes it an lvalue expression, and an lvalue does not bind to an rvalue reference parameter (Buffer&&) directly. std::move(a) casts a to an rvalue reference, making it eligible to bind to that parameter. Without it, the compiler has no way to know the program considers a safe to plunder, so it does not treat a as movable.
Why is C++17's guaranteed copy elision for return Buffer(100); considered different from NRVO for return local; (a named local variable)?
Guaranteed copy elision applies specifically to initializing an object directly from a prvalue, such as the temporary Buffer(100) — the C++17 standard requires the compiler to construct it directly in its final location, calling no copy or move constructor at all. NRVO, eliding the copy or move when a named local variable is returned, is a related but separate optimization that compilers are permitted, but never required, to perform — a program must not assume it will happen and must keep a working move (or copy) constructor available.
A class's move constructor takes over a source object's owning pointer directly and then does not modify the source's pointer afterward. What goes wrong when the source and destination objects are later both destroyed?
If the move constructor fails to null out the source's pointer after taking it over, both the source and destination objects end up believing they own the same resource — effectively the same problem as an uncorrected shallow copy. When both objects are destroyed, their destructors each attempt to release the same resource, resulting in it being released twice, which is undefined behavior. A correct move constructor must leave the source's pointer in a state, typically nullptr, that its destructor can safely process without deleting anything.
Trace through this code. What sequence of messages does it print, and why does constructing c print a different message than constructing b?
Tag a("x"); calls the ordinary constructor, printing 'ctor'. Tag b(a); initializes b from a, which is a named lvalue, so it selects the copy constructor, printing 'copy'. Tag c(std::move(a)); casts a to an rvalue reference with std::move, so it selects the move constructor instead, printing 'move'. Full output: ctor copy move.
Write a class named StringBox that owns a dynamically allocated char array (via new char[length + 1], filled from a const char* constructor argument with a manual loop). Give it a copy constructor (deep copy), a move constructor (noexcept, transferring the pointer and nulling out the source), and a destructor. Demonstrate constructing a StringBox, moving it into a second one with std::move, and printing both objects' data pointers as null-or-not to show the transfer.
The move constructor takes over other.data directly and sets other.data to nullptr, so after StringBox b(std::move(a));, a.data is nullptr while b.data holds the array that used to belong to a, containing "hello". The program prints: a: null b: not null hello.
For each return statement above, state whether the compiler is guaranteed by C++17 to elide the copy/move, only permitted to (but not required to), or neither -- and name the relevant optimization in the permitted case.
return Widget(); in makeTemp initializes the return value directly from a prvalue, so C++17 guarantees copy elision -- no copy or move constructor is called at all. return w; in makeWidget returns a named local object, so the compiler is only permitted, not required, to elide the copy or move -- this is Named Return Value Optimization (NRVO); if the compiler does not perform it, w is instead treated as an rvalue for overload resolution and the move constructor is selected.
Write a function named consumeIfLarge that takes a std::string by value (so the caller's argument is either copied or moved into the parameter depending on how it's called) and returns the string's length as a size_t. Call it once passing a named std::string variable directly (a copy) and once passing std::move of that same variable (a move), printing both lengths and the variable's state after the moved call.
The first call passes text as a named lvalue, so the parameter s is copy-constructed, leaving text unchanged (still "engineering", length 11). The second call passes std::move(text), so the parameter s is move-constructed from text, and text is left in a valid but unspecified state as a std::string -- it must not be read for a specific value at that point, but reassigning it, as the code then does with text = "reused";, is a fully valid operation. Output: 'After copy call, text = engineering, length = 11', then 'length = 11', then 'text is now valid but unspecified; reassigning is safe: reused'.
class Tag
{
public:
Tag(std::string v) : value(v) { std::cout << "ctor "; }
Tag(const Tag& other) : value(other.value) { std::cout << "copy "; }
Tag(Tag&& other) noexcept : value(std::move(other.value)) { std::cout << "move "; }
std::string value;
};
Tag a("x");
Tag b(a);
Tag c(std::move(a));class Widget
{
public:
Widget() = default;
};
Widget makeWidget()
{
Widget w;
return w;
}
Widget makeTemp()
{
return Widget();
}