C++ / Intermediate / Lesson 8
Copy, move, and value semantics
Reason about independent values and ownership transfer, without inferring unspecified moved-from contents or counting optimizations as language guarantees.
On this page
Lesson goals
- Distinguish copy construction, assignment, and move construction.
- Explain what std::move does and what it does not do.
- Apply rule-of-zero and noexcept reasoning to value types.
Copying establishes another value
Value semantics means an object behaves like an independent value under copying, assignment, and destruction. Copy construction creates a new object from an existing one; copy assignment replaces the value of an already existing object. Containers of owning value types naturally compose these operations. The first example copies a vector and edits only the copy, showing separate logical sequences. Do not generalize this to raw pointer members: copying a pointer copies an address, so the pointed-to object remains shared unless a class deliberately defines a deep-copy policy.
#include <iostream>
#include <vector>
int main() {
std::vector<int> original{3, 5};
auto copy = original;
copy[0] = 9;
std::cout << original[0] << " " << copy[0] << "\n";
}
3 9
Changing copy does not change original because vector copies its int elements into independently owned storage.
Names and expiring resources
An lvalue expression identifies an object with identity, such as a named variable. An rvalue can enable operations intended for temporary or expiring resources. std::move is a cast that produces an xvalue expression; it does not itself move bytes, destroy an object, or guarantee selection of a move operation. A named rvalue-reference parameter is still an lvalue expression inside its function. A const source often cannot bind to the usual non-const move overload and may be copied instead. Inspect the selected constructor rather than assuming every std::move is cheap.
Use only promised moved-from properties
A moved-from standard-library object is generally valid but has an unspecified value unless its specification gives a stronger guarantee. It can be destroyed and assigned to, while other operations must satisfy their normal preconditions. Do not print a moved-from string and claim that an empty line is guaranteed. The second example prints only the destination, then explicitly resets the source before observing it. A moved-from unique_ptr is specifically empty, but that fact does not apply universally to strings, vectors, or user-defined classes.
#include <iostream>
#include <string>
#include <utility>
int main() {
std::string source{"report"};
std::string destination = std::move(source);
std::cout << destination << "\n";
source = "new";
std::cout << source << "\n";
}
report
new
The destination receives report. Assigning new gives the source a known value, so no output depends on unspecified moved-from contents.
Let ownership members define the defaults
For ordinary classes containing strings and containers, prefer compiler-generated special members. If a class directly manages a resource, consider destruction, copy construction, copy assignment, move construction, and move assignment together: this is the rule of five. A unique owner may delete copying and implement or default moving. Declaring a destructor can suppress implicit move generation, so a seemingly harmless logging destructor can change performance. Defaulted operations are defined according to member capabilities, and can themselves be deleted when a member cannot support the requested operation.
Promise failure behavior accurately
A genuinely non-throwing move constructor helps containers preserve strong guarantees during reallocation. Implementations may copy elements instead of using a potentially throwing move when copying is available. Mark an operation noexcept only if its complete implementation supports that promise; an escaping exception then calls terminate. Returning a fresh value can benefit from guaranteed C++17-and-later prvalue construction, while named return value optimization remains optional. Avoid return std::move(local) as a default habit because it can prevent named return value optimization and adds complexity without improving ownership clarity.
Common errors and fixes
Code expects a moved-from string to be empty.
- Why it happens
- An observed implementation detail was treated as a contract.
- How to fix it
- Reset the source before relying on a particular value.
- Prevent it next time
- Read the type-specific moved-from guarantee.
Moving a const object still copies.
- Why it happens
- The usual move constructor needs a non-const source reference.
- How to fix it
- Allow mutation only when transfer is intended, or accept copying.
- Prevent it next time
- Use const for invariants rather than applying move mechanically.
A supposedly safe move terminates on failure.
- Why it happens
- An exception escaped a noexcept operation.
- How to fix it
- Remove the false promise or make all invoked operations non-throwing.
- Prevent it next time
- Audit noexcept transitively, including member operations.
Check your understanding
1. After moving a vector into another vector, you need to reuse the source as an empty work buffer. State a portable sequence and the facts you can assert.
Show worked solution
Move-construct the destination, then call source.clear(). You may now assert source.empty(), and subsequently append new work items. Do not assert its capacity or earlier post-move size unless the exact operation specification supplies that guarantee. The destination contains the transferred original elements. Do not retain old element pointers without reviewing the operation and allocator-specific rules.
2. A class contains a string and vector and defines a destructor only to clear both members. Recommend a change and explain its effect on special members.
Show worked solution
Remove the redundant destructor and rely on member destruction. This follows the rule of zero and permits the compiler to generate appropriate copy and move operations under the normal rules. Explicit clearing is unnecessary for resource release. If custom destruction is truly required later, review all copy and move operations together instead of assuming their old availability remains unchanged.
Key takeaways
- std::move enables selection; it does not perform transfer itself.
- Moved-from values need type-specific reasoning.
- Prefer generated special members and truthful noexcept promises.