Skip to content

61 Intermediate The Hundred

return std::move(x) Is Slower

a helping hand the optimizer never asked for

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

Widget make_simple() {
    Widget w;
    return w;
}

Widget make_optimized() {
    Widget w;
    return std::move(w);   // squeeze out that last copy
}

int main() {
    std::cout << "make_simple:\n";
    Widget a = make_simple();
    std::cout << "make_optimized:\n";
    Widget b = make_optimized();
}

Run it. What does each call print?

Answer

make_simple prints nothing — no copy, no move. make_optimized, the one that tries to help, prints move.

Why

return w; makes w eligible for the named return value optimization: the compiler builds w directly in the caller's a, so no copy or move constructor ever runs. NRVO is technically optional, but every mainstream compiler does it — GCC does it here even at -O0. The elision rule requires the return expression to be the plain name of a local; std::move(w) is a function call, not a name, so elision is off the table and the move constructor must run. You paid a move to dodge a copy that could never have happened anyway — a returned local is treated as an rvalue, so the fallback is a move.

GCC flags it with -Wpessimizing-move (part of -Wall): "moving a local object in a return statement prevents copy elision" — and even suggests removing the call.

The fix

Widget make_optimized() {
    Widget w;
    return w;   // NRVO: built in place — zero copies, zero moves
}

Since C++17, returning a prvalue (return Widget{};) is even guaranteed copy-free: elision there is mandatory, not an optimization.

Takeaway: std::move on a return value is a pessimization — return local; is already optimal.

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

Open in Compiler Explorer ↗ Quiz this entry