C++ / MOVE SEMANTICS AND SPECIAL MEMBER FUNCTIONS
Move assignment and leaving sources valid
Write a move assignment operator that releases the target's old resource, steals the source's, and leaves the source destructible and re-assignable.
What you will learn
- Write T& operator=(T&&) noexcept that releases, steals, resets, then returns *this
- Guard self-assignment with an identity check or a member-wise swap
- Leave the source destructible and re-assignable by nulling handles and zeroing sizes
- Pick release-then-steal or swap based on how soon the old resource must be freed
Understanding Move assignment and leaving sources valid
A move constructor starts with raw storage; a move assignment operator starts with a live object that may already own something. That one difference dictates the whole shape of the operator: T& operator=(T&& other) noexcept has to dispose of whatever *this currently holds, then take the source's resource, then return *this because the standard's MoveAssignable requirement (relied on by containers and algorithms) says the result is a reference to the assigned-to object. Omitting the release step is the most common way to turn a move into a leak, since once you overwrite the pointer member nothing in the program still refers to the old allocation.
Leaving the source valid means two concrete things: its destructor must run correctly, and every operation with no preconditions must still work, so you can destroy it, assign a new value to it, or ask it for its size. It does not mean unchanged and it does not mean empty; it means no two objects believe they own the same resource and no member still describes something that is gone. In practice, after copying the pointer out you set the source's pointer to null and its size to zero, which turns its destructor into a harmless no-op and keeps its accessors self-consistent. The reason the requirement is phrased as valid rather than empty is that some types transfer ownership fastest by swapping, and a swap leaves the source holding the target's former value.
Self-move-assignment looks absurd spelled out, but it happens whenever two names, references, or container slots alias one object, and some standard algorithm implementations do move an element onto itself while shuffling elements around. Release-then-steal therefore has to begin with if (this == &other) return *this;, otherwise you free your own resource and then read from the wreckage. A member-wise swap is self-assignment safe for free, but it postpones releasing the target's old resource until the source is destroyed or assigned to again. Choose swap when that delay is harmless, and release-then-steal when the resource is a lock, a socket, or a large buffer that should die at the point of assignment.
<cstddef>
<iostream>
<utility>
class Buffer {
public:
Buffer(std::size_t n, const char* tag)
: data_(new int[n]), size_(n), tag_(tag) {
std::cout << "alloc " << tag_ << " (" << size_ << ")\n";
}
~Buffer() {
std::cout << "free " << tag_ << " (" << size_ << ")\n";
delete[] data_;
}
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_), tag_(other.tag_) {
other.data_ = nullptr;
other.size_ = 0;
other.tag_ = "empty";
}
Buffer& operator=(Buffer&& other) noexcept {
if (this == &other) {
std::cout << "self-assignment, nothing to do\n";
return *this;
}
std::cout << "assign: release " << tag_ << ", take " << other.tag_ << "\n";
delete[] data_; // the target already owns a buffer
data_ = other.data_; // take the source's
size_ = other.size_;
tag_ = other.tag_;
other.data_ = nullptr; // leave the source destructible
other.size_ = 0;
other.tag_ = "empty";
return *this;
}
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
std::size_t size() const { return size_; }
const char* tag() const { return tag_; }
private:
int* data_;
std::size_t size_;
const char* tag_;
};
int main() {
Buffer a(4, "A");
Buffer b(9, "B");
std::cout << "-- a = std::move(b) --\n";
a = std::move(b);
std::cout << "a: " << a.tag() << " " << a.size() << "\n";
std::cout << "b: " << b.tag() << " " << b.size() << "\n";
std::cout << "-- reuse the moved-from b --\n";
b = Buffer(2, "C");
std::cout << "b: " << b.tag() << " " << b.size() << "\n";
std::cout << "-- self-assignment through an alias --\n";
Buffer& alias = a;
a = std::move(alias);
std::cout << "a: " << a.tag() << " " << a.size() << "\n";
std::cout << "-- end of scope --\n";
}
Move assignment must first destroy what the target already owns, then take the source's resource and leave the source in a state its destructor and any later assignment can handle.
Worked examples
Swap-based move assignment defers the release
Implementing move assignment as a member-wise swap is self-assignment safe, but the target's old resource stays alive inside the source until the source dies.
<iostream>
<string>
<utility>
class Handle {
public:
explicit Handle(std::string name) : name_(std::move(name)) {
std::cout << "open " << name_ << "\n";
}
~Handle() {
if (!name_.empty()) std::cout << "close " << name_ << "\n";
}
Handle(Handle&& other) noexcept : name_(std::move(other.name_)) {
other.name_.clear();
}
Handle& operator=(Handle&& other) noexcept {
name_.swap(other.name_); // no identity check needed
return *this;
}
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
private:
std::string name_;
};
int main() {
Handle active("a.log");
Handle pending("b.log");
std::cout << "assign\n";
active = std::move(pending);
std::cout << "after assign\n";
}
Example explained
Line 1name_.swap(other.name_) moves ownership in both directions, so the assignment itself destroys nothing.
Line 2Swapping an object with itself is a no-op, which is why this body needs no if (this == &other) guard.
Line 3No close line appears between "assign" and "after assign": a.log is still open, now owned by pending.
Line 4pending is destroyed first (reverse declaration order) and it holds a.log, so a.log closes before b.log.
Important notes
= default gives a member-wise move assignment that is already correct when every member is itself move-aware (unique_ptr, string, vector); hand-write the body only for raw handles such as a pointer or a file descriptor.
Declaring a move assignment operator makes the implicit copy assignment operator deleted, so a type that should still be copyable has to spell out its copy operations.
Common mistakes
Pasting the move constructor's body into operator=: with no delete[] data_ first, the target's old buffer is never freed and the leak grows with every assignment.
Nulling the source's pointer but leaving its size member unchanged, so a later loop bounded by size() on the moved-from object dereferences a null pointer.
Assuming x = std::move(x) cannot happen: without an identity check, release-then-steal frees the object's own buffer, silently loses the value, and any member read from other after the release is a use-after-free.
Try it yourself
Change, predict, then run
Copy the Buffer example, delete the if (this == &other) early return, and run a = std::move(alias) again to see the object lose its buffer. Then rewrite the operator as three member swaps and confirm the same call leaves a holding B with 9 ints.
Open the C++ workspaceCheck your understanding
A Logger owns an open file, and its move assignment operator is implemented by swapping members with the source. The program runs active = std::move(pending); and then keeps pending alive for another hour. What is the observable consequence?
- The file active held before the assignment stays open for that hour, because pending now owns it and closes it only when destroyed
- Nothing observable: swapping members and releasing then stealing behave identically
- Undefined behaviour, because a move assignment operator must leave the source empty
- active's old file is closed by the swap itself, and pending is left empty
Show answer
A swap only relocates ownership; nothing is destroyed at the assignment, so the target's former file is released only when the source is destroyed or assigned to again. Option 2 is tempting because the rule is often taught as "leave the source empty", but the actual requirement is that the source stays valid, meaning destructible and assignable, and holding someone else's file satisfies that.