C++ / MOVE SEMANTICS AND SPECIAL MEMBER FUNCTIONS
Move constructors and stealing resources safely
Write a move constructor that takes ownership of a resource, leaves the source safely destructible, and is marked noexcept so containers actually move.
What you will learn
- Write T(T&& other) noexcept and initialise members from other's handles
- Blank the source's handle and reset the fields that describe it
- Apply std::move to owning members: inside the move constructor they are lvalues
- Recognise that a missing noexcept makes vector copy elements when it reallocates
Understanding Move constructors and stealing resources safely
A move constructor has the signature T(T&&) and runs when a new object is initialised from an rvalue of the same type. Its job is not to duplicate the owned data but to transfer the bookkeeping: read the source's pointer, length, file descriptor or whatever identifies the resource, store those values in the new object, then overwrite them in the source. That is why move-constructing an object owning 100 MB costs the same as one owning 10 bytes; either way you copy a pointer and a couple of integers.
The safety half of stealing is that second step. The moved-from object is still alive and its destructor will still run, so if you read data_ out of the source but leave other.data_ pointing at the same block, both destructors call delete[] on it. Setting other.data_ = nullptr and other.size_ = 0 turns the source's destructor into a no-op, because delete[] nullptr is defined to do nothing. The invariant to hold in your head: when the constructor returns, exactly one object owns the resource and every object in the program is still destructible.
Two details trip up hand-written move constructors. Members named through other are lvalues (other has type T&&, but the expression other.name is an lvalue), so name(other.name) calls that member's copy constructor and you need name(std::move(other.name)) instead. And because reassigning pointers cannot fail, mark the constructor noexcept: a container relocating its elements must not lose data if a relocation throws part-way, so it routes through std::move_if_noexcept, which falls back to the copy constructor when your move constructor is potentially throwing.
<cstddef>
<iostream>
<utility>
class Buffer {
public:
explicit Buffer(std::size_t n) : data_(new int[n]), size_(n) {
for (std::size_t i = 0; i < n; ++i) data_[i] = static_cast<int>(i);
std::cout << "ctor: allocated " << size_ << " ints\n";
}
Buffer(const Buffer& other) : data_(new int[other.size_]), size_(other.size_) {
for (std::size_t i = 0; i < size_; ++i) data_[i] = other.data_[i];
std::cout << "copy ctor: duplicated " << size_ << " ints\n";
}
// Take the handle, then blank the source so its destructor frees nothing.
Buffer(Buffer&& other) noexcept : data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
std::cout << "move ctor: took over " << size_ << " ints, no allocation\n";
}
~Buffer() {
std::cout << "dtor: " << state() << ", size " << size_ << "\n";
delete[] data_; // delete[] nullptr is a no-op
}
const char* state() const { return data_ ? "owning" : "empty"; }
private:
int* data_;
std::size_t size_;
};
int main() {
Buffer a(4);
Buffer b(a); // lvalue -> copy constructor
Buffer c(std::move(a)); // rvalue -> move constructor
std::cout << "a is " << a.state()
<< ", b is " << b.state()
<< ", c is " << c.state() << "\n";
}
A move constructor transfers ownership by copying the source's handle and then blanking it, so exactly one object owns the resource and both objects stay destructible.
Worked examples
Members are lvalues inside the move constructor
Shows that initialising a member from other.member copies it, and that std::move is what turns the member into something a move constructor can claim.
<iostream>
<utility>
struct Loud {
int v;
explicit Loud(int x) : v(x) {}
Loud(const Loud& o) : v(o.v) { std::cout << " Loud copied\n"; }
Loud(Loud&& o) noexcept : v(o.v) { o.v = -1; std::cout << " Loud moved\n"; }
};
struct Careless {
Loud part;
explicit Careless(int x) : part(x) {}
Careless(Careless&& o) noexcept : part(o.part) {} // o.part is an lvalue
};
struct Careful {
Loud part;
explicit Careful(int x) : part(x) {}
Careful(Careful&& o) noexcept : part(std::move(o.part)) {}
};
int main() {
Careless a(1);
std::cout << "moving Careless:\n";
Careless a2(std::move(a));
std::cout << "source member holds " << a.part.v << "\n";
Careful b(2);
std::cout << "moving Careful:\n";
Careful b2(std::move(b));
std::cout << "source member holds " << b.part.v << "\n";
}
Example explained
Line 1o has type Careless&&, but the expression o.part is an lvalue, so overload resolution selects Loud(const Loud&).
Line 2std::move(o.part) casts that lvalue to an xvalue, which is what makes Loud(Loud&&) viable.
Line 3"source member holds 1" proves the Careless version transferred nothing: the member was duplicated and the source left intact.
Line 4Careless is declared noexcept while running a copy constructor that could allocate, so a throwing copy there would call std::terminate.
Why the noexcept matters
Uses std::move_if_noexcept, the utility containers rely on when relocating elements, to show a potentially-throwing move constructor being demoted to a copy.
<iostream>
<type_traits>
<utility>
struct Safe {
Safe() = default;
Safe(const Safe&) { std::cout << "Safe: copy\n"; }
Safe(Safe&&) noexcept { std::cout << "Safe: move\n"; }
};
struct Risky {
Risky() = default;
Risky(const Risky&) { std::cout << "Risky: copy\n"; }
Risky(Risky&&) { std::cout << "Risky: move\n"; } // no noexcept
};
int main() {
Safe s;
Risky r;
std::cout << "relocating one element of each:\n";
Safe s2(std::move_if_noexcept(s));
Risky r2(std::move_if_noexcept(r));
std::cout << std::boolalpha
<< "Safe nothrow-move-constructible: "
<< std::is_nothrow_move_constructible<Safe>::value << "\n"
<< "Risky nothrow-move-constructible: "
<< std::is_nothrow_move_constructible<Risky>::value << "\n";
}
Example explained
Line 1std::move_if_noexcept(s) yields Safe&& because Safe's move constructor is noexcept, so the move constructor runs.
Line 2The same call on r yields const Risky&, so the copy constructor runs; the only difference between the two types is one keyword.
Line 3This is the decision std::vector makes when it outgrows its capacity: it cannot relocate half a buffer and then have a move throw, so a throwing move is demoted to a copy.
Line 4Adding noexcept to Risky's move constructor changes the second output line to "Risky: move".
Important notes
Empty is a convenient target state for the source, not a rule; it only has to be destructible and usable by operations with no preconditions.
Declaring a move constructor makes the implicit copy constructor and copy assignment operator deleted and suppresses any generated move assignment, so decide about the other special members deliberately.
Common mistakes
Nulling other.data_ but forgetting other.size_ = 0: the source then advertises four elements behind a null pointer, and the first loop that reads it crashes.
Writing vec(other.vec) instead of vec(std::move(other.vec)): it compiles, deep-copies every element, and the "move" costs exactly as much as a copy.
Adding delete[] other.data_ to the move constructor to tidy up the source: that frees the block the new object just took over, leaving it dangling.
Try it yourself
Change, predict, then run
Write a Matrix class owning double* cells with rows and cols, give it a noexcept move constructor that takes the pointer and then zeroes cells, rows and cols in the source. Move-construct from a 3x3 matrix, print each object's rows, cols and whether its pointer is null, and add a print in the destructor to confirm the moved-from object frees nothing.
Open the C++ workspaceCheck your understanding
A move constructor copies data_ and size_ out of other but never modifies other's members. It compiles, and the newly built object reads its data correctly. What is wrong with it?
- Nothing: the compiler blanks the moved-from object once the constructor returns.
- Both objects now hold the same pointer, so the second destructor to run frees an already-freed block.
- The move constructor silently behaves as a copy constructor, so no resource is transferred at all.
- The moved-from object may never be touched again, which makes the program ill-formed.
Show answer
Nothing resets the source for you. The moved-from object is an ordinary object whose destructor runs at the end of its scope, and it still points at the block the new object owns, so that block is released twice and the surviving object dangles from the first release onward. Option 3 describes a different defect (forgetting std::move on a member, or omitting noexcept), which costs performance rather than memory safety.