82 Advanced The Hundred
The Move That Copies
your move constructor is present, correct, and ignored
#include <iostream>
#include <vector>
struct Pixel {
Pixel() = default;
Pixel(const Pixel&) { std::cout << "copy\n"; }
Pixel(Pixel&&) { std::cout << "move\n"; }
};
int main() {
std::vector<Pixel> pixels;
pixels.reserve(3);
for (int i = 0; i < 3; ++i)
pixels.emplace_back();
std::cout << "-- growing past capacity --\n";
pixels.emplace_back(); // relocates the three old elements... by move, surely?
}
Run it. How do the three old elements reach the new buffer?
Answer
Every element is duplicated. The move constructor sits right there and never runs.
Why¶
Growing past capacity means transferring every element to a fresh buffer, and vector
promises the strong exception guarantee: if anything throws mid-transfer, the vector is
left exactly as it was. A throwing move would wreck that: half the elements gutted
out of the old buffer, and no way back. So the transfer uses std::move_if_noexcept,
which moves only when the move constructor is noexcept and otherwise copies. Yours
isn't marked, so as far as vector is concerned it might throw — and every
reallocation silently copies the whole vector. -Wall -Wextra say nothing (GCC 11.5,
Clang 18); clang-tidy's performance-noexcept-move-constructor does. A move-only type
still gets moved: with no copy to fall back on, the guarantee is quietly downgraded.
The fix¶
One keyword turns the three copy lines into move:
Better yet, Pixel(Pixel&&) = default; — a defaulted move is noexcept whenever the
members' moves are, one more reason to prefer the rule of zero.
Takeaway: a move constructor without noexcept is a copy in disguise wherever the
strong exception guarantee matters — mark your moves noexcept.
Try it: g++ -std=c++17 main.cpp -o demo && ./demo — then add noexcept to the move constructor and run again.