Move Semantics
Move semantics (C++11) allow a program to transfer ownership of resources rather than copy them. Before C++11, returning a vector from a function copied all its elements. With move semantics, the function transfers the internal buffer — an O(1) operation instead of O(n). Modern C++ relies on move semantics for performance throughout the standard library.
Lvalues and Rvalues
int x = 42; // x is an lvalue (has a name, has an address)
int y = x; // Copies x into y
// Rvalues are temporaries — no name, no persistent address
int z = x + 1; // (x + 1) is an rvalue temporary
// After this line, the temporary is gone
// Lvalue reference — binds to named objects
int& lref = x; // OK
// int& lref2 = 42; // ERROR — 42 is an rvalue
// Rvalue reference (C++11) — binds to temporaries
int&& rref = 42; // OK
int&& rref2 = x + 1; // OK — binds to temporary
// int&& rref3 = x; // ERROR — x is an lvalueThe Cost of Copying
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class BigData {
vector<int> data;
public:
BigData(size_t n) : data(n, 0) {
cout << "Constructed (" << n << " elements)\n";
}
// Copy constructor — copies all n elements
BigData(const BigData& other) : data(other.data) {
cout << "Copied (" << data.size() << " elements)\n";
}
// Move constructor — just swaps the internal pointer
BigData(BigData&& other) noexcept : data(move(other.data)) {
cout << "Moved (O(1) — just transferred pointer)\n";
}
size_t size() const { return data.size(); }
};
int main() {
BigData original(1000000); // Constructed (1,000,000 elements)
BigData copy = original; // Copied (1,000,000 elements) — expensive!
BigData moved = move(original); // Moved — O(1), just a pointer swap
cout << "copy.size() = " << copy.size() << "\n"; // 1000000
cout << "moved.size() = " << moved.size() << "\n"; // 1000000
cout << "original.size() = " << original.size() << "\n"; // 0 (moved-from)
return 0;
}Move Constructor and Move Assignment
#include <iostream>
#include <utility>
using namespace std;
class Buffer {
int* data;
size_t size_;
public:
Buffer(size_t n) : data(new int[n]()), size_(n) {
cout << "Constructed " << n << " ints\n";
}
// Copy constructor — deep copy
Buffer(const Buffer& other) : data(new int[other.size_]), size_(other.size_) {
copy(other.data, other.data + size_, data);
cout << "Copied " << size_ << " ints\n";
}
// Move constructor — steal the pointer
Buffer(Buffer&& other) noexcept
: data(other.data), size_(other.size_) {
other.data = nullptr; // Leave source in valid empty state
other.size_ = 0;
cout << "Moved (O(1))\n";
}
// Move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data; // Free existing resource
data = other.data;
size_ = other.size_;
other.data = nullptr;
other.size_ = 0;
cout << "Move-assigned\n";
}
return *this;
}
~Buffer() {
delete[] data;
cout << "Destroyed " << size_ << " ints\n";
}
size_t size() const { return size_; }
};
int main() {
Buffer b1(100); // Constructed 100 ints
Buffer b2 = move(b1); // Moved (O(1)) — b1 is now empty
cout << b1.size() << "\n"; // 0
cout << b2.size() << "\n"; // 100
Buffer b3(50);
b3 = move(b2); // Move-assigned
cout << b3.size() << "\n"; // 100
return 0;
}std::move
std::move is just a cast — it casts an lvalue to an rvalue reference, enabling move semantics:
#include <iostream>
#include <string>
#include <utility>
using namespace std;
int main() {
string s1 = "Hello, World!";
// Without move — copies the string
string s2 = s1;
cout << s1 << "\n"; // Hello, World! (unchanged)
cout << s2 << "\n"; // Hello, World!
// With std::move — transfers the buffer
string s3 = move(s1);
cout << s1 << "\n"; // "" (moved-from — valid but unspecified)
cout << s3 << "\n"; // Hello, World!
// Inserting into vector with move
vector<string> v;
string big = "Large string that should not be copied";
v.push_back(move(big)); // Moves — no copy
cout << big << "\n"; // "" (moved-from)
return 0;
}Warning: After std::move(x), the moved-from object x is in a valid but unspecified state. You can assign to it again or destroy it — but don't read its value.
Return Value Optimisation (RVO)
When you return a local object by value, the compiler often eliminates the copy entirely:
#include <iostream>
#include <vector>
using namespace std;
vector<int> makeVector(int n) {
vector<int> result;
for (int i = 0; i < n; i++) result.push_back(i);
return result; // RVO — the compiler constructs result directly in the caller
}
int main() {
auto v = makeVector(5); // No copy, no move — constructed in place
for (int x : v) cout << x << " ";
cout << "\n"; // 0 1 2 3 4
return 0;
}Named Return Value Optimisation (NRVO) applies when you return a named local variable. Modern compilers perform this optimization automatically — don't force return std::move(result) as it can disable RVO.
Perfect Forwarding
Perfect forwarding preserves the value category (lvalue/rvalue) of arguments when passing them to another function:
#include <iostream>
#include <utility>
using namespace std;
void process(int& x) { cout << "lvalue: " << x << "\n"; }
void process(int&& x) { cout << "rvalue: " << x << "\n"; }
// Template with forwarding reference (T&&)
template <typename T>
void forwardToProcess(T&& arg) {
process(forward<T>(arg)); // Preserves lvalue/rvalue nature
}
int main() {
int x = 42;
forwardToProcess(x); // lvalue: 42 (lvalue — x has a name)
forwardToProcess(99); // rvalue: 99 (rvalue — temporary)
forwardToProcess(x+1); // rvalue: 43 (rvalue — temporary)
return 0;
}std::forward is used in variadic templates and factory functions like make_unique:
template <typename T, typename... Args>
unique_ptr<T> myMakeUnique(Args&&... args) {
return unique_ptr<T>(new T(forward<Args>(args)...));
}noexcept and Move Semantics
Mark move constructors and move assignments as noexcept — this allows the standard library to use move operations in critical paths:
class Safe {
int* data;
public:
// noexcept — tells std::vector it's safe to move during reallocation
Safe(Safe&& other) noexcept : data(other.data) {
other.data = nullptr;
}
Safe& operator=(Safe&& other) noexcept {
swap(data, other.data);
return *this;
}
};Without noexcept, std::vector falls back to copying during reallocation for exception safety.
Practical Example: Move-Enabled String Builder
#include <iostream>
#include <string>
#include <utility>
#include <vector>
using namespace std;
class StringBuilder {
vector<string> parts;
public:
// Append — copy
StringBuilder& append(const string& s) {
parts.push_back(s);
return *this;
}
// Append — move (avoids copy for temporaries)
StringBuilder& append(string&& s) {
parts.push_back(move(s));
return *this;
}
// Build — returns assembled string (RVO applies)
string build() const {
string result;
for (const auto& part : parts) result += part;
return result;
}
// Move the builder itself
StringBuilder(StringBuilder&&) noexcept = default;
StringBuilder& operator=(StringBuilder&&) noexcept = default;
StringBuilder() = default;
};
StringBuilder makeQuery(const string& table, const vector<string>& columns) {
StringBuilder sb;
sb.append("SELECT ");
for (size_t i = 0; i < columns.size(); i++) {
sb.append(columns[i]);
if (i + 1 < columns.size()) sb.append(", ");
}
sb.append(" FROM ").append(table).append(";");
return sb; // NRVO — no copy
}
int main() {
vector<string> cols = {"id", "name", "email"};
StringBuilder qb = makeQuery("users", cols);
cout << qb.build() << "\n";
// SELECT id, name, email FROM users;
// Move the builder
StringBuilder qb2 = move(qb);
cout << qb2.build() << "\n"; // Still works
// SELECT id, name, email FROM users;
return 0;
}Summary
- An lvalue has a name and address; an rvalue is a temporary without persistent storage
- The move constructor and move assignment steal resources from temporaries rather than copying them
std::move(x)castsxto an rvalue reference — it does not actually move anything by itself- After moving from an object, it is in a valid but unspecified state — don't read it, but you can reassign or destroy it
- RVO/NRVO eliminates copy/move entirely when returning local objects — never write
return std::move(local) std::forwardin template code preserves lvalue/rvalue nature — the basis of perfect forwarding- Always mark move operations
noexcept—std::vectorand other containers rely on this for optimised reallocations
Next, we'll explore the C++ Standard Template Library — starting with vectors and arrays, the most commonly used containers.