Skip to content

← Copy, Move, and the Rule of Zero step 2 of 5

Medium Primitives

std::move does not move anything

The name is a lie and it has cost the industry a lot of time.

template <typename T>
constexpr std::remove_reference_t<T>&& move(T&& t) noexcept {
    return static_cast<std::remove_reference_t<T>&&>(t);
}

That is the entire implementation. std::move is a cast. It generates no instructions, transfers nothing, and leaves its argument exactly as it was.

What it does is change what the next line is allowed to do. Casting to an rvalue reference makes the expression eligible for the move constructor or move assignment operator, and it is that function — written by the author of the type — which actually steals the pointer and leaves the source empty.

std::move is a request for permission. The move happens in the constructor that accepts it, or it does not happen at all.

Three ways the request is silently denied

1. The source is const.

const Payload& p = items[0];
out.push_back(std::move(p));    // casts to const Payload&&

A move constructor takes Payload&&; a const Payload&& cannot bind to it. Overload resolution falls back to the copy constructor, which takes const Payload& and accepts anything. The code compiles, says move, and copies. This is the most common of the three and the hardest to see.

2. The destination takes const&.

void store(const Payload& p);
store(std::move(payload));      // copies inside store, if it copies at all

There is no move overload to select, so the cast selects the same function it would have anyway.

3. The type has no move operations. Then there is nothing to select and every “move” is a copy. Track 3.4 is about when that happens.

And one place you should not write it

return std::move(local);   // worse than `return local;`

Returning a local by name already moves — better, it usually elides the move entirely and constructs the value directly in the caller. Writing std::move there turns an expression the compiler could elide into one it cannot, so the “optimisation” makes it slower. Return the name.

Your task

std::vector<Payload> take(std::vector<Payload>& parts, int limit);

Move the first limit payloads out of parts into a new vector and return it. If limit exceeds the size, take everything; if it is zero or less, take nothing. parts keeps its size — you are emptying elements, not erasing them.

The starter already calls std::move and still copies every element, which is the lesson stated as a bug. Payload counts its copies and the harness prints the count; get it to zero.

Run the gate when you are stuck. performance-move-const-arg names the line and explains itself better than most compiler messages.