C++ Move Semantics: Copy vs Move Explained
이 글의 핵심
C++11 move semantics: rvalue references, std::move, Rule of Five, noexcept move constructors—copy vs move performance and safe usage patterns.
Introduction: Why Copies Hurt, and What C++11 Fixed
For decades, C++ made expensive copies the default way to “hand off” data. A std::vector with a million int values, a std::string backing a large buffer, or a file handle inside a custom class all paid O(n) (or worse) when passed by value or returned—unless the compiler could prove that copying was unnecessary. That model is correct for value semantics, but it punishes unique ownership and transient values that exist only to be consumed.
C++11 introduced move semantics: overloads that bind to rvalue references (T&&) and transfer resources (pointers, handles, internal buffers) from a source object to a target, leaving the source in a valid but unspecified state. The run-time cost drops to O(1) for many standard types, without abandoning RAII or deterministic destruction.
This article walks through the mechanics—rvalue references, move construction and assignment, std::move vs std::forward, exceptions and noexcept, RVO, universal references, std::move_if_noexcept, and how the standard library and real string/vector-style types behave. The goal is not to memorize std::move in every function, but to know when copies happen, when moves are selected, and how to implement resource-owning types correctly.
I thought I understood move semantics. Then I debugged a moved-from string
I was comfortable with the rules on paper: std::move is a cast, the library leaves moved-from objects in a valid but unspecified state, and you are supposed to reassign or destroy the source. That sounded like a checkbox exercise until I was staring at a defect where a std::string had been passed through a helper, “moved” into a second object, and then the original was still read later in the same function. The value was not guaranteed empty on our toolchain; a colleague’s mental model of “moved == cleared” was wrong for the standard’s contract. The fix was not more std::move—it was to stop treating the first string as a readable buffer after the handoff, and to structure the code so only one name owned the data at a time. That afternoon taught me that move semantics are as much about API boundaries and discipline as they are about casting. If a move does not make ownership obvious in the source, the bug will come back as soon as someone refactors around it.
The copy problem in one picture
Before C++11, handing a large std::vector to another owner meant a full copy unless you passed by pointer or reference. After C++11, an rvalue (often a temporary) can bind to a move constructor that reuses the internal storage: pointer swap, not element-by-element copy. std::move does not perform that transfer—it only names a value as an rvalue so overload resolution can pick a move. The constructor or assignment operator then performs the actual resource handoff.
#include <utility>
#include <vector>
void illustrate() {
std::vector<int> v(1'000'000, 42);
std::vector<int> a = v; // lvalue: copy
std::vector<int> b = std::move(v); // xvalue: move (v may be empty afterward)
(void)a;
(void)b;
}
Lvalue and rvalue (intuition)
- An lvalue typically has a name and a stable address; you can take
&xin many cases. - A prvalue (“pure rvalue”) is a temporary produced by an expression, such as
x + 1or a literal20. - An xvalue is a kind of rvalue, e.g. the result of
std::move(named)—it is a “dying” value that is allowed to be pilfered from.
#include <string>
int x = 10; // x is an lvalue
int y = x + 1; // prvalue on the right in x + 1
// int& r = 10; // error: non-const lvalue ref to rvalue
const int& cr = 10; // OK: const& can extend lifetime of a temporary
(void)y; (void)cr;
std::string s1 = "hi";
std::string s2 = s1; // lvalue s1: copy
std::string s3 = s1 + "!"; // prvalue: move from temporary into s3
(void)s2; (void)s3;
Move semantics are about which constructor runs when the right-hand side is an rvalue (including xvalues from std::move). The next section details T&& and binding.
Rvalue References: && Syntax and Binding Rules
A glvalue has identity (e.g. a named variable). A prvalue is a pure temporary. An xvalue is a kind of rvalue (e.g. the result of std::move(x)). Rvalue references are written T&& in contexts where they denote “this parameter may bind to a temporary or to something we are allowed to pilfer from.”
Binding rules (simplified):
- A non-
constlvalue referenceT&binds only to lvalues. - An rvalue reference
T&&binds to rvalues (including xvalues). const T&is special: it can bind to lvalues and rvalues, sometimes extending the lifetime of a temporary.
int x = 10;
int& lr = x; // OK: lvalue
// int& z = 20; // error: 20 is an rvalue
int&& rr = 20; // OK: 20 is an rvalue
// int&& w = x; // error: x is an lvalue
const int& cr = 20; // OK: const& can bind to a temporary
Forwarding references (sometimes called universal references) use T&& in a deduced context (e.g. a template parameter). They can bind to either lvalues or rvalues; std::forward preserves the value category—covered in a dedicated section below.
template<typename T>
void foo(T&& arg); // T&& is often a forwarding reference when T is deduced
For a named rvalue reference like String&& other in a move constructor, other is itself an lvalue (it has a name). That is why you must write std::move(other) when passing it to subobjects that should steal from it.
Move Constructor: Implementation Details
The move constructor has the form T(T&& other) (optionally noexcept). It should:
- Pilfer the resource from
other(copy internal pointers, sizes, etc.). - Leave
otherin a valid, destroyable state—typically by nulling out pointers and zeroing sizes so the destructor ofotherdoes not double-free.
#include <cstring>
#include <iostream>
class String {
char* data_{nullptr};
std::size_t size_{0};
public:
explicit String(const char* s) {
size_ = std::strlen(s);
data_ = new char[size_ + 1];
std::strcpy(data_, s);
}
~String() { delete[] data_; }
String(const String& other) {
size_ = other.size_;
data_ = new char[size_ + 1];
std::strcpy(data_, other.data_);
}
String(String&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
};
Details that matter in production code:
- Self-move in the constructor is rare, but if
this == &other, you must not null out before reading—treat it as a no-op or handle explicitly. - Exception safety: a throwing move can leave programs using strong exception guarantees in a bad state; prefer
noexceptwhen possible (see below). - Invariants: any class invariant must hold for the moved-from object; “empty” is a common choice.
Move Assignment: Self-Assignment and this != &other
Move assignment is typically T& operator=(T&& other) noexcept. The classic guard if (this != &other) prevents self-assignment from deleting your own resource before reading the source. Without it, a = std::move(a) can corrupt state.
String& operator=(String&& other) noexcept {
if (this == &other) {
return *this;
}
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
return *this;
}
A common copy-and-swap idiom for copy assignment (swap with a temporary) gives strong exception safety; for move assignment, the “destroy old, steal new” pattern above is standard when both operations are noexcept.
std::move: An Unconditional Cast to Rvalue
std::move does not move anything. It unconditionally casts its argument to an rvalue (more precisely, to an xvalue) so that overload resolution prefers move constructors and move assignment operators when they exist.
#include <utility>
#include <vector>
void demo() {
std::vector<int> a{1, 2, 3, 4, 5};
std::vector<int> b = std::move(a); // move ctor of vector<int>
(void)b;
// a is valid but may be empty; do not read from a without re-assigning
}
Conceptually (C++20 std::move is allowed to be more constrained, but the idea is the same):
template<typename T>
[[nodiscard]] constexpr std::remove_reference_t<T>&& move(T&& t) noexcept {
return static_cast<std::remove_reference_t<T>&&>(t);
}
Use std::move when you are relinquishing ownership of a local or member and passing it to a sink (constructor, emplace, container insertion). It is a signal in code review: after this line, the object should be treated as empty unless reinitialized.
std::move is overused. Here’s when you actually need it
I see std::move sprinkled in codebases like salt: on every return, on every push_back, on parameters that are already temporaries. Most of that noise adds nothing. You need an explicit std::move when you have a named lvalue and you are transferring it into a sink—constructor, emplace*, a function taking by value, or a unique_ptr handoff. You usually do not need it when the expression is already a temporary: vec.push_back(make_string()); will move (or elide) without your help, and return local; for a local by value should stay bare so NRVO and elision can win. The middle ground—push_back with a std::string you built in a loop—is where the cast earns its pay: the variable is a named lvalue, and the vector should steal its buffer. If your only reason for std::move is “performance,” but the value category already selects a move, delete the cast and keep the invariants. Opinionated bottom line: treat every std::move in review as a claim that the name is dead after this line; if the next line still reads that name for its value, the cast is wrong or the design is.
std::forward: Conditional Cast and Perfect Forwarding
While std::move always produces an rvalue, std::forward<T> restores the value category of a forwarding reference T&& arg when T is a template parameter: if the caller passed an lvalue, forward returns an lvalue; if an rvalue, it returns an rvalue. That lets one wrapper function forward arguments to emplace, constructors, or other overload sets without extra copies.
#include <iostream>
#include <utility>
void process(int& x) { std::cout << "lvalue " << x << "\n"; }
void process(int&& x) { std::cout << "rvalue " << x << "\n"; }
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
void demo2() {
int n = 10;
wrapper(n); // T is int&; forward => lvalue
wrapper(20); // T is int; forward => rvalue
}
Variadics are the same idea: std::forward<Args>(args)... in std::make_unique, std::vector::emplace_back, and so on.
Moved-From State: Valid but Unspecified
After a successful move, the source object must remain in a state where:
- Destruction is well-defined and does not double-free.
- The object may be assigned a new value or moved from again.
Reading from a moved-from std::string or std::vector is technically allowed for many types (e.g. size() is often 0) but is not a portable contract unless the type documents it. Always follow the standard library’s valid but unspecified rule: do not use a moved-from value except to destroy it or reassign it, unless the type’s documentation says otherwise (e.g. some types clear to empty state).
Rule of Five: A Complete Example
If you manage a raw resource, you should usually declare all five special members explicitly (or =default/delete them with clear intent):
- Destructor
- Copy constructor
- Copy assignment
- Move constructor
- Move assignment
Omitting move operations forces extra copies; omitting the destructor with raw owning pointers leaks. Rule of Zero (using smart pointers and standard containers) is often better—but when you own raw resources, the Rule of Five is the checklist.
#include <iostream>
#include <string>
class Resource {
int* data_{nullptr};
public:
explicit Resource(int v) : data_(new int(v)) {
std::cout << "ctor " << *data_ << "\n";
}
~Resource() {
std::cout << "dtor " << (data_ ? std::to_string(*data_) : "null") << "\n";
delete data_;
}
Resource(const Resource& o) : data_(new int(*o.data_)) {
std::cout << "copy ctor\n";
}
Resource& operator=(const Resource& o) {
if (this == &o) return *this;
*data_ = *o.data_;
std::cout << "copy assign\n";
return *this;
}
Resource(Resource&& o) noexcept : data_(o.data_) {
o.data_ = nullptr;
std::cout << "move ctor\n";
}
Resource& operator=(Resource&& o) noexcept {
if (this == &o) return *this;
delete data_;
data_ = o.data_;
o.data_ = nullptr;
std::cout << "move assign\n";
return *this;
}
};
void rule_of_five_demo() {
Resource a(1);
Resource b = a; // copy
Resource c = std::move(a); // move; a is null-initialized
Resource d(2);
d = b; // copy assign
Resource e(3);
d = std::move(e); // move assign
(void)c;
}
noexcept on Move Operations: Why It Matters
Move constructors and move assignment that are not noexcept can cause std::vector (and other containers) to copy instead of move when reallocation is required, because the strong exception guarantee for many operations is only tenable if moving elements cannot throw.
struct Bad {
Bad(Bad&&) { /* might throw */ } // not noexcept
};
struct Good {
std::vector<int> v;
Good(Good&& o) noexcept : v(std::move(o.v)) {}
};
If std::is_nothrow_move_constructible_v<T> is false, vector may use copy operations during growth. For performance-critical move-only types, marking moves noexcept (when accurate) is essential.
Note: a move that lies about noexcept and then throws is undefined behavior in contexts that rely on the guarantee—do not mark noexcept unless the implementation truly cannot throw.
When Copy Happens vs When Move Happens
Assignment and parameters: If you write T b = a; and a is an lvalue, you get a copy unless the type is weird. If you write T b = std::move(a);, overload resolution can pick a move if one exists. Passing an lvalue T into void f(T x) still copies into the parameter; the copy happens because the source has a name and lifetime outside the call. The same f with std::move(t) passes an xvalue, so a move is in play when the type supports it. Returns: return local; is the sweet spot for RVO and NRVO; if elision does not apply, the compiler will often still move the local. Containers: Pushing an lvalue with push_back(x) copies; pushing a prvalue (or a moved-from lvalue you no longer need) can move. A const object never feeds a non-const rvalue reference, so std::move on a const T& still copies—I have watched people “optimize” that and wonder why the profiler is flat. unique_ptr is the blunt lesson: there is no copy; you move or the program does not compile. For scalars, move and copy are typically the same machine instructions; the interesting cases are owning types and handles.
RVO and How It Interacts with Move
Return value optimization (RVO) and named return value optimization (NRVO) construct the return object directly in the caller’s space, eliding even move operations. C++17 guarantees copy elision in some return cases (prvalue guaranteed copy elision).
#include <vector>
std::vector<int> make1() {
return std::vector<int>{1, 2, 3}; // often elided
}
std::vector<int> make2() {
std::vector<int> v{1, 2, 3};
return v; // often NRVO; if not, move
}
// Anti-pattern: suppresses RVO in many compilers for local named objects
std::vector<int> make3_bad() {
std::vector<int> v{1, 2, 3};
return std::move(v); // can force a move, blocking NRVO
}
Guideline: for a local return by value, return v; is preferred over return std::move(v);. Use std::move on returns mainly when returning members or subobjects, or when the type is not eligible for NRVO. See the related post on copy elision in relatedPosts.
Template Deduction: Universal References in Brief
Scott Meyers’ “universal reference” term refers to T&& where T is a deduced template parameter for a function or method. Reference collapsing (&+&, &+&&, etc.) plus the special template argument deduction rule for T&& in a call such as f(expr) is what lets a single f accept both lvalues and rvalues, with T deduced as U& or U respectively. That is how std::forward works with template <typename T> void f(T&&).
Pitfall: In a class template, T&& in a member function is not always a universal reference; it is only a forwarding reference in deduced contexts. For a template member void bar(U&& u) of template <typename T> class C, U is deduced and U&& is a forwarding reference. Inside C<T>, T&& when T is the class’s parameter is an rvalue reference to T, not a universal reference.
#include <string>
#include <utility>
struct Bag {
std::string s_;
// U is deduced; U&& here is a forwarding reference (not std::string&&).
template<typename U>
void set(U&& u) { s_ = std::forward<U>(u); }
};
(The forwarding reference pattern is a function or member template of the form template <typename U> void f(U&& u) with deduced U, not the class’s own T when you write C<T>::method(T&&)—that is usually a plain rvalue reference to T.)
Move-Only Types: unique_ptr and std::thread
Some types are not copyable by design. std::unique_ptr, std::thread, and many I/O or OS resource handles are move-only: ownership is transferred, not shared.
#include <memory>
#include <thread>
void consume(std::unique_ptr<int> p) { (void)(*p); }
void move_only() {
auto p = std::make_unique<int>(42);
// consume(p); // error: no copy
consume(std::move(p));
(void)(p.get() == nullptr);
}
Move-only types enforce unique ownership in the type system and are natural clients of std::move at API boundaries.
Moving in Loops: Performance Patterns
When building a std::vector of non-trivial types, reserve to avoid repeated reallocations, and move lvalues you no longer need into the container.
#include <string>
#include <vector>
void fill_names(std::vector<std::string>& out) {
out.clear();
out.reserve(1000);
for (int i = 0; i < 1000; ++i) {
std::string s = "name_" + std::to_string(i);
out.push_back(std::move(s)); // move into vector
}
}
emplace_back can avoid a temporary std::string altogether. Prefer emplace when construction arguments are available, push_back(std::move(x)) when you already hold a complete object.
Range-based for over a vector of move-only types: iterating by value without move will not compile for unique_ptr; use auto& or const auto& as appropriate, or move in algorithms with care.
Moving Members: Constructor Initializer Lists
In constructors, base and member initialization with std::move must use the moved form when consuming parameters:
#include <string>
#include <vector>
struct Widget {
std::vector<int> v_;
std::string s_;
explicit Widget(std::vector<int> v, std::string s)
: v_(std::move(v)), s_(std::move(s)) {}
};
Default member initializers run before the constructor body; prefer initializing members in the member init list with std::move of parameters. For a templated forwarding constructor, use std::forward appropriate to each member.
std::move_if_noexcept and the Strong Exception Guarantee
When a container reallocation or certain algorithms must offer the strong exception guarantee, the implementation may copy (slow but rollback-friendly) if the move is not noexcept, and move if it is. std::move_if_noexcept(x) returns an lvalue reference (copy) or rvalue (move) based on noexceptness of the move.
#include <type_traits>
#include <utility>
#include <vector>
template <typename T>
void reallocate_aux(std::vector<T>& v) {
(void)std::move_if_noexcept(*v.begin()); // illustration: use in generic code
}
User code rarely calls std::move_if_noexcept directly, but the principle matters: if your type’s move can throw, generic code may copy your elements, hurting performance. Making moves noexcept when correct aligns with the standard library’s expectations.
Benchmarks: Copy vs Move (Illustrative)
Microbenchmarks depend on hardware, allocator, and compiler. The pattern below is useful to reproduce on your machine. Expect copy to allocate and memcpy a large block; move to swap a few pointers for std::vector.
#include <chrono>
#include <iostream>
#include <vector>
int main() {
using clock = std::chrono::high_resolution_clock;
const int n = 1'000'000;
std::vector<int> a(n, 42);
auto t0 = clock::now();
std::vector<int> b = a; // copy
auto t1 = clock::now();
std::vector<int> c = std::move(b); // move from b; b empty
auto t2 = clock::now();
auto us = [](auto t) {
return std::chrono::duration_cast<std::chrono::microseconds>(t).count();
};
std::cout << "copy: " << us(t1 - t0) << " us\n";
std::cout << "move: " << us(t2 - t1) << " us\n";
(void)c;
return 0;
}
On typical desktops, the move time is orders of magnitude smaller than deep copy for large vectors. Profile your own workloads; for small vectors, inline storage or SSO strings may change the story.
Common Mistakes
std::move on const Objects
std::move on a const lvalue of type T yields a const rvalue; overload resolution will pick the copy constructor (const T&), not the move, because T&& cannot bind to const T for the move.
const std::vector<int> c{1,2,3};
std::vector<int> d = std::move(c); // still copies
Fix: use a non-const object if you need a move, or a different design (e.g. const_cast is almost never the right tool here).
return std::move(local) (Usually Wrong)
For a function-local object returned by value, return local; enables RVO; return std::move(local); can prevent it and add an unnecessary move. Prefer plain return for locals. Exception: returning a member by value, or rvalue with move-enabled idiom, may use std::move in specific patterns.
Use-After-Move
Using a moved-from object for anything other than destroy or assign is a bug. Do not read s[0] on a moved-from std::string without checking your library’s postconditions or reassigning.
Missing noexcept on Cheap Moves
If your class is used in std::vector and the move is cheap and nothrow-capable, mark it noexcept. Otherwise you may get silent copies on reallocation.
Standard Library: Move-Aware Containers and Algorithms
std::vector, std::deque, std::string, and node-based containers (e.g. std::list, std::map) are move-aware: operations like insert, emplace, and push_back use moves when the source is an rvalue. std::swap is specialized to move in many cases. std::optional and std::variant use moves when storing move-only types.
Algorithms may move when writing to OutputIterators or when moving from one range to another (std::move + std::move algorithm). std::make_unique / std::make_shared move or forward constructor arguments.
Mistakes I made
Rule of Zero vs Five: I used to “half-implement” special members and wonder why we leaked or double-freed. Now I either own nothing raw (Rule of Zero) or I own the full Rule of Five story explicitly—destructor, copy, move, both assignments—or I delete what must not exist.
Lying to noexcept: I marked moves noexcept because the container “wanted” it before I proved the body could not throw. The right move is: implement moves that are actually nothrow, then mark them; the standard library can then relocate without silently copying.
Return locals: I reached for return std::move(x); because it felt faster. In most local-by-value returns it is the wrong trade: you risk blocking NRVO and add ceremony. I reserve explicit moves for members, subobjects, or cases elision cannot cover.
Overusing std::move: I sprayed the cast on temporaries and on every return. I now add it only when a named lvalue is being sunk—and I treat the name as semantically dead afterward.
Forwarding: I have called std::move on a template parameter that was a forwarding reference and watched the wrong overload fire. The habit I keep: std::forward<T> for T&& in generic code, std::move only for definite sinks.
Moved-from use: I assumed “string is empty” after std::move and read from it. The standard does not promise that; I only read after reassign, or I use the object as a destructive sink.
Const and copies: I tried to std::move a const big object and expected a move. I got a copy, which is correct—only non-const rvalues match the usual move reference.
emplace vs move: I defaulted to push_back(std::move(s)) when emplace_back with arguments would have skipped building s at all. Now I emplace when I can, move when the object is already materialized.
Red flags in code review
Growth is hot on your T but only in vector: I look for a move constructor that is not noexcept when it could be. The implementation may be copying elements on reallocation to preserve the strong exception guarantee. Confirm with std::is_nothrow_move_constructible_v<T> and a profiler.
std::move did not “move”: If the source is const, the result is a const rvalue; expect copy. If the type has no move from non-const rvalues, you also get a copy. Fix the constness, the type’s interface, or accept the cost.
Crashes or double free after a move: I hunt for a custom T with a move that leaves a bad pointer, or self-move in assignment that clears before reading. Invariants for the moved-from object must still admit safe destruction.
Generic wrapper always uses std::move: On T&& that deduces from the caller, std::forward<T> is the review pass; bare std::move drops the lvalue case and the wrong process overload can run.
return got slower with std::move: On local by-value return, return std::move(x) is a smell—it can block elision. I ask for plain return x; unless the author documents a case NRVO cannot cover.
unique_ptr will not go through: Someone is still trying to copy a unique owner. I expect std::move into the sink, or a factory that returns by value, not a const reference to a unique_ptr in the wrong place.
Real Examples: String and vector-Style Growth
The following minimal String and dynamic array types illustrate the same ideas as std::string and std::vector: three pointer-sized fields to steal (data, size, capacity) vs O(n) char copy.
Minimal String with Move and Copy
#include <algorithm>
#include <cstring>
#include <iostream>
#include <utility>
class String {
char* data_{nullptr};
std::size_t size_{0};
std::size_t cap_{0};
static char* alloc(std::size_t n) { return new char[n]; }
void release() { delete[] data_; data_ = nullptr; size_ = cap_ = 0; }
public:
explicit String(const char* s) {
size_ = std::strlen(s);
cap_ = size_ + 1;
data_ = alloc(cap_);
std::memcpy(data_, s, cap_);
}
~String() { release(); }
String(const String& o) {
size_ = o.size_;
cap_ = o.cap_;
data_ = alloc(cap_);
std::memcpy(data_, o.data_, cap_);
}
String& operator=(const String& o) {
if (this == &o) return *this;
String tmp(o);
swap(tmp);
return *this;
}
String(String&& o) noexcept : data_(o.data_), size_(o.size_), cap_(o.cap_) {
o.data_ = nullptr;
o.size_ = o.cap_ = 0;
}
String& operator=(String&& o) noexcept {
if (this == &o) return *this;
release();
data_ = o.data_;
size_ = o.size_;
cap_ = o.cap_;
o.data_ = nullptr;
o.size_ = o.cap_ = 0;
return *this;
}
void swap(String& o) noexcept {
using std::swap;
swap(data_, o.data_);
swap(size_, o.size_);
swap(cap_, o.cap_);
}
};
void string_demo() {
String a("Hello");
String b = std::move(a);
(void)b;
}
vector-Style Reallocation: Move if noexcept, Else Copy
Conceptually, when std::vector<T> grows, it allocates a new buffer and move-constructs each element (if noexcept move) or copy-constructs (if a throwing move would break the strong guarantee). That is why noexcept move matters for user-defined T.
// Conceptual reallocation (not complete std::vector):
// 1) Allocate new storage
// 2) For i in [0, old_size):
// construct new[i] as std::move_if_noexcept(old[i]) // copy if move might throw
// destroy old[i]
// 3) construct new[old_size] from the new element
// 4) Deallocate old storage
//
// `std::move_if_noexcept(x)` is roughly:
// noexcept(…move…) ? static_cast<remove_reference_t<T>&&>(x) : x
// so a throwing move causes a copy, preserving rollback if an exception is thrown mid-reloc.
Real implementations use Allocator traits, destroy in reverse order, and allocator-aware construction; the important lesson for this post is the move_if_noexcept branch in step 2.
Practical Patterns: Factory, Containers, and swap
Factory returning by value + moving into members:
#include <string>
#include <vector>
struct Database {
std::vector<std::string> data_;
explicit Database(std::vector<std::string> d) : data_(std::move(d)) {}
};
Database make_db() {
std::vector<std::string> rows;
rows.push_back("r1");
rows.push_back("r2");
return Database(std::move(rows));
}
Container: push_back(std::move(x)) or emplace_back for strings you no longer need.
swap: std::swap on vectors is noexcept and O(1) for pointer swap—often used in copy-and-swap assignment.
Resource Manager: unique_ptr in a vector
#include <iostream>
#include <memory>
#include <string>
#include <vector>
class ResourceManager {
std::vector<std::unique_ptr<std::string>> resources_;
public:
void add(std::unique_ptr<std::string> r) {
resources_.push_back(std::move(r));
}
std::unique_ptr<std::string> take(std::size_t i) {
if (i >= resources_.size()) return nullptr;
auto p = std::move(resources_[i]);
resources_.erase(resources_.begin() + static_cast<std::ptrdiff_t>(i));
return p;
}
};
void res_demo() {
ResourceManager m;
m.add(std::make_unique<std::string>("a"));
m.add(std::make_unique<std::string>("b"));
auto x = m.take(0);
(void)x;
}
Big Object in a vector (Move on push_back)
#include <iostream>
#include <vector>
struct BigObject {
std::vector<int> data_;
explicit BigObject(int n) : data_(static_cast<std::size_t>(n), 0) {
std::cout << "ctor " << n << "\n";
}
BigObject(const BigObject& o) : data_(o.data_) {
std::cout << "copy\n";
}
BigObject(BigObject&& o) noexcept : data_(std::move(o.data_)) {
std::cout << "move\n";
}
};
void big_demo() {
std::vector<BigObject> v;
v.reserve(2);
v.push_back(BigObject(10)); // move from temporary
BigObject b(20);
v.push_back(std::move(b));
}
Perfect Forwarding (Minimal, Compilable)
#include <iostream>
#include <utility>
struct Thing {
Thing() = default;
Thing(const Thing&) { std::cout << "copy Thing\n"; }
Thing(Thing&&) noexcept { std::cout << "move Thing\n"; }
};
void emit(Thing& ) { std::cout << "lvalue (Thing&)\n"; }
void emit(Thing&&) { std::cout << "rvalue (Thing&&)\n"; }
template <typename T>
void relay(T&& x) {
emit(std::forward<T>(x));
}
int main() {
Thing t;
relay(t); // T is Thing&; forward => lvalue, emit(Thing&)
relay(Thing{}); // T is Thing; forward => rvalue, emit(Thing&&)
}
Summary: Key Takeaways
- Move semantics transfer resources from a source to a new owner in O(1) for many types.
- Rvalue references (
T&&) select move overloads; namedT&¶meters are lvalues in the function body. std::moveis a cast;std::forwardpreserves value category for forwarding references.- Moved-from objects are valid but have unspecified state unless the type documents otherwise.
- Rule of Five (or Rule of Zero with smart pointers) keeps ownership correct.
noexcepton moves enables moves invectorreallocation; missing it can mean copies.- RVO can beat move; avoid
return std::move(local)for locals. - Universal references and
forwardare required for generic code that should not add copies. - Move-only types (e.g.
unique_ptr) express unique ownership. std::move_if_noexcept, containers, and strong guarantees connect exception safety to move vs copy in reallocation.
Related reading (on this site)
- C++ Rvalue vs Lvalue | Value categories guide
- C++ Perfect Forwarding
- C++ Copy Elision | RVO, NRVO
- C++ move constructor (deep dive)
- C++ algorithm copy and move algorithms
- C++ RVO and NRVO
FAQ (from this post)
- Practical use: use moves when you no longer need the source, when inserting into containers, when implementing owning types, and at boundaries with
std::unique_ptr. Do not rely on the contents of a moved-from object. - If something is slow: check
noexcepton your move constructor, look for hidden copies ofconstor small vector reallocation, and profile. - Deeper study: cppreference: std::move, value categories, and your standard library implementation (for
vectorreallocation) are authoritative complements to this article.