Skip to content

59 Intermediate The Hundred

The Destructor That Deleted Your Move

two structs, one extra line, and only one of them moves

struct Payload {
    Payload() = default;
    Payload(const Payload&) { std::cout << "copy\n"; }
    Payload(Payload&&) noexcept { std::cout << "move\n"; }
};

struct Box {
    Payload contents;
};
struct Chest {
    Payload contents;
    ~Chest() = default;   // Chest owns nothing, but be explicit about cleanup
};

template <class T> void ship(T) {}

int main() {
    Box b;
    Chest c;
    std::cout << "Box   -> ";
    ship(std::move(b));
    std::cout << "Chest -> ";
    ship(std::move(c));
}

Run it. Do both hand-offs move?

Answer

Only the Box moves. Chest has no move constructor at all — that defaulted destructor took it away, and std::move fell back on the copy.

Why

A class gets an implicit move constructor and move assignment only if it declares none of the other special members — and a destructor counts, even = default. Chest declares one, so it has no moves; the copy constructor is still there, and it binds std::move(c)'s rvalue without complaint. Even the traits play along: std::is_move_constructible_v<Chest> is true — it only asks whether some constructor accepts an rvalue — while the nothrow version is false, so std::vector<Chest> copies on reallocation too (entry 82). -Wall -Wextra say nothing (GCC 11), and -Wdeprecated-copy-dtor (entry 58) ignores a defaulted destructor.

The fix

Delete the line. A class that declares no special members gets all five, correct and free:

struct Chest {
    Payload contents;   // rule of zero — and the move is back
};

If the destructor must stay, = default the rest — and mind the cascade: restoring the two moves deletes the two copies, and any constructor you declare removes Chest().

Takeaway: touching one special member silently changes which others exist — write all five, or none.

Try it: g++ -std=c++17 main.cpp -o demo && ./demo

Open in Compiler Explorer ↗ Quiz this entry