Card 1 / 5
A move constructor takes the source by rvalue reference (Buffer&&) and steals its resources instead of copying them. Implement it for Buffer: take over other.data, set moved = true, and leave other empty.
| call | b.data |
b.moved |
a.data afterwards |
|---|---|---|---|
Buffer a("hello"); Buffer b(std::move(a)); |
"hello" |
true |
"" |
Buffer a("x"); (no move) |
β | a.moved == false |
"x" |
Buffer b(std::move(a)); a.data = "second"; |
"first" |
true |
"second" β a moved-from object may be reused |
Use the member-initializer list: Buffer(Buffer&& other) noexcept : data(std::move(other.data)), moved(true) { other.data.clear(); }. Inside the constructor other is a named variable, so it is an lvalue β without std::move you would copy its string. noexcept matters: std::vector only moves elements during a reallocation if the move constructor promises not to throw.
// A type that remembers HOW it was constructed. Write the move constructor:
// steal `other.data` (no copy), set moved = true, and leave `other` empty.
struct Buffer {
std::string data;
bool moved = false;
explicit Buffer(std::string d) : data(std::move(d)) {}
Buffer(Buffer&& other) noexcept
: data(std::move(other.data)), moved(true) {
other.data.clear(); // moved-from state: valid AND predictable
}
};Card 2 / 5
Move assignment (a = std::move(b)) replaces an object that already exists. Implement Buffer::operator=(Buffer&&): steal other.data, leave other empty, and return *this. Guard against self-assignment β a = std::move(a) must leave a unchanged.
| before | call | b.data after |
a.data after |
|---|---|---|---|
a = {1,2,3}, b empty |
b = std::move(a); |
{1,2,3} |
{} |
a = {7}, b empty |
Buffer& r = (b = std::move(a)); |
{7} β and &r == &b |
{} |
a = {4,5} |
a = std::move(a); |
β | {4,5} unchanged |
Three steps: if (this != &other) guards the self-move; data = std::move(other.data); other.data.clear(); does the theft and leaves a predictable moved-from state; return *this; is the conventional result of every operator=. Because the class declares a move constructor, the compiler deletes the copy operations β Buffer becomes move-only. When your class has nothing special to do, prefer Buffer& operator=(Buffer&&) noexcept = default; and let the members move themselves.
// Write the move ASSIGNMENT operator: steal `other.data`, leave `other`
// empty, and return *this. Guard against self-assignment
// (`a = std::move(a)`) β it must leave `a` unchanged.
struct Buffer {
std::vector<int> data;
Buffer() = default;
explicit Buffer(std::vector<int> d) : data(std::move(d)) {}
Buffer(Buffer&&) noexcept = default;
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) { // self-move: nothing to steal from
data = std::move(other.data);
other.data.clear(); // valid, predictable moved-from state
}
return *this; // the conventional return for operator=
}
};Card 3 / 5
After std::string b = std::move(a); the object a still exists β it will run its destructor like any other variable β but the standard only promises it is in a valid but unspecified state. You may do anything that has no precondition on its contents; you may not assume what those contents are.
std::string a = "data";
std::string b = std::move(a);
a = "fresh"; // fine: assignment gives a a known value again
a.clear(); // fine: no precondition
char c = a[0]; // NOT fine: requires a non-empty string, which is not guaranteedIn practice most standard types are left empty (std::string, std::vector, std::unique_ptr becomes null), and your own move constructors should aim for the same predictability β but code that relies on emptiness is relying on an implementation detail. The safe habits: reassign, clear()/reset(), or just let it go out of scope.
After std::string b = std::move(a);, which of these uses of a are guaranteed safe by the standard?
a = "new value";Correct answerSafe. Assignment has no precondition; it overwrites whatever unspecified state was left behind and a is fully known again.
a be destroyed normally when it goes out of scope.Correct answerSafe. "Valid" means exactly this: the destructor can run. A move must never leave an object that cannot be destroyed.
a.clear(); and then use a as an empty string.Correct answerSafe. clear() has no precondition on the current contents, and afterwards the state is known (empty).
char c = a[0]; expecting the old first character.Not safe. The old contents were moved away, and operator[] requires the index to be within the current size β which is unspecified and typically 0.
if (a.empty()) β relying on it being true.Not guaranteed. Calling empty() is allowed, but the standard does not promise the answer is true for a moved-from std::string; libstdc++ happens to leave it empty, other implementations need not.
Card 4 / 5
Move every element of src onto the end of dst, then leave src empty. The element type Token (declared above the editable region) has its copy constructor deleted and its move constructor defaulted, so a solution that copies will not compile β each element must be moved.
dst before |
src before |
dst after |
src after |
|---|---|---|---|
[a] |
[b, c] |
[a, b, c] |
[] |
[x] |
[] |
[x] |
[] |
[] |
[only] |
[only] |
[] |
Loop for (Token& t : src) dst.push_back(std::move(t)); β the loop variable is an lvalue, so std::move is what makes push_back pick its Token&& overload. Afterwards src still holds moved-from shells; src.clear() removes them. Reserving dst.size() + src.size() first avoids a reallocation per push. Production shortcut: dst.insert(dst.end(), std::make_move_iterator(src.begin()), std::make_move_iterator(src.end())); does the same loop in one call.
// Move every Token from `src` onto the end of `dst` (in order), then leave
// `src` empty. Token cannot be copied, so each element must be MOVED.
void drain_into(std::vector<Token>& dst, std::vector<Token>& src) {
dst.reserve(dst.size() + src.size()); // one allocation instead of one per push_back
for (Token& t : src) {
dst.push_back(std::move(t)); // std::move casts t to Token&&: push_back moves it
}
src.clear(); // drop the moved-from shells: src is truly empty
}Card 5 / 5
Write a generic move_swap(a, b) that exchanges two values using moves only β the classic three-move swap. It has to work for any movable type, including one that cannot be copied at all.
a before |
b before |
a after |
b after |
|---|---|---|---|
1 |
2 |
2 |
1 |
"left" |
"right" |
"right" |
"left" |
unique_ptr to 10 |
unique_ptr to 20 |
owns 20 |
owns 10 |
T tmp = std::move(a); a = std::move(b); b = std::move(tmp); β three moves, zero copies. Each std::move is needed because a, b and tmp are all named variables (lvalues); without it every line would copy, and the unique_ptr case would not compile. This is exactly what std::swap does for you β in real code call that, and write using std::swap; swap(a, b); so a type's own faster overload is found.
// Exchange `a` and `b` using MOVES only β the classic three-move swap.
// It must work for any movable type, including types that cannot be copied.
template <typename T>
void move_swap(T& a, T& b) {
T tmp = std::move(a); // 1. tmp steals a's contents (a is now empty/unspecified)
a = std::move(b); // 2. a steals b's contents
b = std::move(tmp); // 3. b steals what used to be a's
}