Moving an object in C++ means transferring the resources it owns to another object and leaving the source in a valid but unspecified state. A move is not a copy: it is what lets a buffer, a string or a file handle change hands without duplicating the underlying data. Since C++11 the compiler uses moves automatically in most situations where it previously copied, which is why modern C++ can return large objects by value without a performance penalty.
What std::move actually does
std::move does not move anything. It is a cast that converts an lvalue into an rvalue, which is the signal that triggers the move constructor or move assignment operator.
#include
#include
#include
class Buffer {
public:
explicit Buffer(const std::string& label)
: data_(new int[4]{1, 2, 3, 4}), label_(label) {
std::cout << "constructed " << label_ << '\n';
}
Buffer(const Buffer& other)
: data_(new int[4]), label_(other.label_ + " (copy)") {
std::cout << "copy constructed " << label_ << '\n';
}
Buffer(Buffer&& other) noexcept
: data_(other.data_), label_(other.label_ + " (moved)") {
other.data_ = nullptr;
std::cout << "move constructed " << label_ << '\n';
}
~Buffer() {
delete[] data_;
std::cout << "destroyed " << label_ << '\n';
}
int* data_;
private:
std::string label_;
};
int main() {
Buffer source("source");
Buffer destination = std::move(source);
std::cout << "destination data: " << destination.data_[0] << '\n';
return 0;
}The output shows that a move happened rather than a copy:
constructed source
move constructed source (moved)
destination data: 1
destroyed source (moved)
destroyed sourceNotice that the buffer was not duplicated. The pointer was transferred and the source was left with a null pointer, so its destructor does nothing.
The moved-from state
A moved-from object is in a valid but unspecified state. Valid means you may still call its destructor, assign to it, or move from it again. Unspecified means you may not rely on what it contains — it might be empty, it might hold the old value, and the exact behaviour is up to the type.
#include
#include
int main() {
std::string first = "a long piece of text";
std::string second = std::move(first);
std::cout << "second: " << second << '\n';
std::cout << "first is empty?: " << first.empty() << '\n';
// The moved-from string is still usable: assigning to it works.
first = "reused";
std::cout << "after reuse: " << first << '\n';
return 0;
}The output shows the standard library's typical behaviour, where a moved-from string is left empty:
second: a long piece of text
first is empty?: 1
after reuse: reusedThe rule is to treat a moved-from object as a fresh object you may assign to, and never to read its contents expecting anything in particular.
Move assignment
Move assignment is the same idea applied to an object that already exists. The old resource is released and the new one is taken over.
#include
#include
#include
class Buffer {
public:
explicit Buffer(const std::string& label)
: data_(new int[4]{1, 2, 3, 4}), label_(label) {}
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_; // release what we had
label_ = std::move(other.label_);
data_ = other.data_; // take over what they had
other.data_ = nullptr;
}
return *this;
}
~Buffer() { delete[] data_; }
int* data_;
private:
std::string label_;
};
int main() {
Buffer source("source");
Buffer destination("destination");
destination = std::move(source);
std::cout << "destination data: " << destination.data_[0] << '\n';
return 0;
}The output shows the destination now holding the buffer:
destination data: 1The noexcept requirement
A move constructor and a move assignment operator should be marked noexcept. The reason is that std::vector reallocates its storage by moving the elements, and if a move can throw, the vector has to fall back to copying to preserve its contents.
#include
#include
#include
#include
class Buffer {
public:
Buffer() : data_(new int[4]{1, 2, 3, 4}) {}
Buffer(Buffer&& other) noexcept
: data_(other.data_) {
other.data_ = nullptr;
}
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
other.data_ = nullptr;
}
return *this;
}
Buffer(const Buffer& other) : data_(new int[4]) {
for (int i = 0; i < 4; ++i) {
data_[i] = other.data_[i];
}
}
~Buffer() { delete[] data_; }
int* data_;
private:
int size_ = 0;
};
int main() {
std::vector fleet;
for (int i = 0; i < 4; ++i) {
fleet.emplace_back();
}
std::cout << "buffers: " << fleet.size() << '\n';
std::cout << "first value: " << fleet[0].data_[0] << '\n';
return 0;
}Marking the move noexcept lets the vector move the elements into the new storage rather than copying them, which is both faster and the only way the buffer pointers stay unique.
Moving on return
Returning a local variable by value was expensive in old C++, because the return value had to be copied out of the function. Since C++11 the copy is elided, and in C++17 returning a named local is guaranteed to be moved.
#include
#include
class Buffer {
public:
Buffer() : data_(new int[4]{7, 8, 9, 10}) {}
Buffer(const Buffer& other) : data_(new int[4]) {
for (int i = 0; i < 4; ++i) {
data_[i] = other.data_[i];
}
}
Buffer(Buffer&& other) noexcept : data_(other.data_) {
other.data_ = nullptr;
}
~Buffer() { delete[] data_; }
int first() const { return data_[0]; }
private:
int* data_;
};
Buffer make_buffer() {
Buffer local;
return local; // moved out, not copied
}
int main() {
Buffer buffer = make_buffer();
std::cout << "first: " << buffer.first() << '\n';
return 0;
}The output shows the value survived the move:
first: 7Returning a large object by value is now perfectly reasonable, and writing std::move on the returned value would actually make things worse by preventing the elision.
Quick reference
std::moveis a cast to rvalue; the move constructor or move assignment does the work.- A move transfers ownership rather than duplicating the data.
- A moved-from object is valid but unspecified: assign to it, do not read it.
- Mark move operations
noexceptso containers can move rather than copy. - Returning a local variable is moved (and usually elided) in modern C++.
- Do not write
std::moveon a return value.
One more habit: when you write a move constructor, take the source object by value rather than by reference. The parameter is then moved into the member, and the source is left in a known, valid state without any manual pointer shuffling.
Try it yourself: write a class that owns a buffer and prints a message in its copy constructor, its move constructor and its destructor. Copy one object and move another, and compare the messages. Then put the class in a [[std::vector]] that grows, and check whether the elements are copied or moved when the vector reallocates. Finally, remove [[noexcept]] from the move and watch the behaviour change.