Move Semantics in C++
Frequently Asked Questions
QWhat is a "moved-from" object allowed to do?
QWhen does the compiler move automatically (implicit moves)?
QWhat happens if I call std::move on a const object?
QWhat is the difference between RVO/NRVO and move semantics?
#include <iostream>
#include <format>
#include <string>
#include <vector>
#include <utility>
#include <cstring>VALUE CATEGORIES Deep Dive
Watch out: std::move does NOT move -- it casts to an rvalue reference. The actual move happens in the move constructor / move assignment operator. -----------------------------------------------
class Buffer {
char* data_;
std::size_t size_;
std::string name_;
public:
// Constructor
Buffer(std::string name, std::size_t size)
: data_(new char[size]), size_(size), name_(std::move(name)) {
std::memset(data_, 0, size_);
std::cout << std::format(" [{}] Constructed ({} bytes)\n", name_, size_);
}
// Destructor
~Buffer() {
if (data_) {
std::cout << std::format(" [{}] Destroyed\n", name_);
}
delete[] data_;
}
// ---- Copy constructor (expensive: allocates + copies) ----
Buffer(const Buffer& other)
: data_(new char[other.size_]),
size_(other.size_),
name_(other.name_ + " (copy)") {
std::memcpy(data_, other.data_, size_);
std::cout << std::format(" [{}] COPIED ({} bytes allocated)\n",
name_, size_);
}
// ---- Move constructor (cheap: just pointer swap) ----
//STD::MOVE Deep Dive
std::move does NOT move anything. It is purely a cast:
template<typename T>
constexpr std::remove_reference_t<T>&& move(T&& x) noexcept {
return static_cast<std::remove_reference_t<T>&&>(x);
}
Conceptually: T&& move(T& x) { return static_cast<T&&>(x); }
All it does is change the value category of 'x' from lvalue to
xvalue (eXpiring value). This makes the expression eligible for
binding to T&& parameters, so the compiler selects the move
constructor instead of the copy constructor.
The *actual* resource transfer (stealing the pointer, nulling the
source) happens right here in the move constructor that YOU write.
Watch out: after std::move, the source object is in a
"valid but unspecified" state. The C++ standard only guarantees
that you can safely destroy it or assign a new value to it.
Do NOT read from it expecting any particular contents. Buffer(Buffer&& other) noexcept
: data_(other.data_), // Steal the pointer
size_(other.size_),
name_(std::move(other.name_) + " (moved)") {
other.data_ = nullptr; // Leave source in valid empty state
other.size_ = 0;
std::cout << std::format(" [{}] MOVED (zero allocation)\n", name_);
}
// ---- Copy assignment ----
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data_;
data_ = new char[other.size_];
size_ = other.size_;
std::memcpy(data_, other.data_, size_);
name_ = other.name_ + " (copy=)";
std::cout << std::format(" [{}] Copy assigned\n", name_);
}
return *this;
}
// ---- Move assignment ----
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
name_ = std::move(other.name_) + " (move=)";
other.data_ = nullptr;
other.size_ = 0;
std::cout << std::format(" [{}] Move assigned\n", name_);
}
return *this;
}
[[nodiscard]] std::size_t size() const { return size_; }
[[nodiscard]] const std::string& name() const { return name_; }
};2 Functions demonstrating when moves happen
Functions demonstrating when moves happen.
Use this when it cleanly solves the problem in front of you.
of a local variable -- it inhibits NRVO (named return value optimization), making performance WORSE, not better.
Use it in code like: Buffer create_buffer() {.
of a local variable -- it inhibits NRVO (named return value optimization), making performance WORSE, not better.
Watch out: do NOT std::move the return value
Buffer create_buffer() {
Buffer b("factory", 1024);
return b; // NRVO (Named Return Value Optimization) may elide the move
}
void take_ownership(Buffer b) {
std::cout << std::format(" Took ownership of: {}\n", b.name());
// b is destroyed at end of function
}3 std::move with STL containers
is in a valid-but-unspecified state -- you can assign to it or destroy it, but do NOT read from it expecting any particular value.
Use this when it cleanly solves the problem in front of you.
Moving strings and vectors avoids deep copies.
Use it in code like: void stl_move_demo() {.
Moving strings and vectors avoids deep copies.
is in a valid-but-unspecified state -- you can assign to it or destroy it, but do NOT read from it expecting any particular value.
Watch out: after std::move, the source object
void stl_move_demo() {
std::cout << "\n--- STL Move Semantics ---\n";
std::string source = "Hello, this is a long string that uses heap memory";
std::cout << std::format("Before move: source = '{}'\n", source);
std::string dest = std::move(source); // Pointer swap, O(1)
std::cout << std::format("After move: dest = '{}'\n", dest);
std::cout << std::format("After move: source = '{}' (valid but unspecified)\n",
source);
// Moving elements into a vector
std::vector<std::string> names;
std::string name = "Alice";
names.push_back(std::move(name)); // Move instead of copy
names.emplace_back("Bob"); // Construct in-place (even better)
std::cout << std::format("names: [{}, {}]\n", names[0], names[1]);
}4 Perfect forwarding with std::forward
Perfect forwarding with std::forward.
Use this when it cleanly solves the problem in front of you.
Preserves the value category (lvalue/rvalue) of arguments.
Follow the code pattern in this section and keep usage explicit.
Preserves the value category (lvalue/rvalue) of arguments.
//
// HOW IT WORKS: PERFECT FORWARDING
//
// In the signature template<typename T> void wrapper(T&& arg),
// T&& is a "forwarding reference" (sometimes called "universal
// reference") -- NOT an rvalue reference -- because T is being
// deduced by the compiler.
//
// Reference collapsing rules determine what T&& becomes:
//
// If the caller passes an lvalue (e.g., a named variable):
// T is deduced as int&
// T&& = int& && = int& (reference collapsing)
// -> arg is an lvalue reference
//
// If the caller passes an rvalue (e.g., std::move(x) or a temp):
// T is deduced as int
// T&& = int&& (no collapsing needed)
// -> arg is an rvalue reference
//
// The problem: inside the function, 'arg' is always an lvalue
// (because it has a name), regardless of what the caller passed.
// If you just wrote take_ownership(arg); it would ALWAYS copy.
//
// std::forward<T>(arg) casts 'arg' back to its original category:
// - If T = int& : forward returns int& (stays lvalue)
// - If T = int : forward returns int&& (restored to rvalue)
//
// Without std::forward, the rvalue-ness of the original argument
// is lost the moment it binds to the named parameter 'arg'.
// Forward restores it, enabling the callee (take_ownership) to
// select the move constructor when the caller intended a move.template<typename T>
void wrapper(T&& arg) {
// std::forward<T> passes lvalues as lvalues, rvalues as rvalues
take_ownership(std::forward<T>(arg));
}int main() {
std::cout << "--- Copy vs Move ---\n";
Buffer original("orig", 1024);
// Copy: allocates new memory and copies bytes
std::cout << "\nCopying:\n";
Buffer copied = original;
// Move: just steals the pointer (no allocation!)
std::cout << "\nMoving:\n";
Buffer moved = std::move(original);
// 'original' is now in a valid but empty state
// Return value optimization
std::cout << "\n--- Return Value (NRVO) ---\n";
auto buf = create_buffer();
// Transfer ownership to a function
std::cout << "\n--- Transfer Ownership ---\n";
take_ownership(std::move(buf));
// STL containers
stl_move_demo();
// Perfect forwarding
std::cout << "\n--- Perfect Forwarding ---\n";
Buffer fwd_buf("forward-me", 512);
wrapper(std::move(fwd_buf)); // Forwarded as rvalue -> move
return 0;
}