Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 10 of 12

std::move is the worst-named function in the standard library.

std::move does not move anything. It is a cast.

It takes an object and produces an rvalue reference to it, which changes nothing at run time. All it does is let overload resolution pick the move constructor or move assignment instead of the copy ones. The moving, if any, is done by those functions.

A reasonable name would have been rvalue_cast.

#include <iostream>
#include <utility>
#include <string>

void takes(const std::string& s) { std::cout << "copy overload: " << s << "\n"; }
void takes(std::string&& s) { std::cout << "move overload: " << s << "\n"; }

int main() {
    std::string a = "hello";

    takes(a);                  // lvalue -> copy overload
    takes(std::move(a));       // std::move casts -> move overload

    // Nothing was moved by std::move itself. `takes` did not steal anything
    // either, because it only printed. So `a` is untouched:
    std::cout << "a is still: " << a << "\n";
    return 0;
}
copy overload: hello
move overload: hello
a is still: hello

That last line is the point. std::move(a) was called, and a still holds "hello" — because nothing that actually moves was invoked.

When it does cause a move

#include <iostream>
#include <utility>
#include <string>

int main() {
    std::string source = "a fairly long string that will not fit in a small buffer";

    std::string copied = source;              // copy: both hold the text
    std::cout << "after copy,  source size = " << source.size() << "\n";

    std::string moved = std::move(source);    // move constructor runs
    std::cout << "after move,  moved size  = " << moved.size() << "\n";
    std::cout << "after move,  source size = " << source.size() << "\n";
    return 0;
}
after copy,  source size = 56
after move,  moved size  = 56
after move,  source size = 0

Here source really was emptied — not by std::move, but by std::string's move constructor, which std::move allowed to be selected.

The distinction matters because it explains every confusing case: if no move constructor runs, nothing moves.

The three ways it does nothing

1. The type has no move operations. It falls back to copying, silently:

#include <iostream>
#include <utility>

// Copyable, but with no move operations of its own.
struct OnlyCopyable {
    int value;
    OnlyCopyable(int v) : value(v) {}
    OnlyCopyable(const OnlyCopyable& other) : value(other.value) {
        std::cout << "  copied\n";
    }
};

int main() {
    OnlyCopyable a(1);
    std::cout << "constructing with std::move:\n";
    OnlyCopyable b = std::move(a);      // no move ctor exists -> copy ctor
    std::cout << "a.value is still " << a.value << ", b.value " << b.value << "\n";
    return 0;
}
constructing with std::move:
  copied
a.value is still 1, b.value 1

std::move compiled fine and achieved nothing. There is no error and no warning — the cast is legal, the copy constructor is simply the best match.

2. The source is const. A move constructor takes T&&, and a const T cannot bind to it:

#include <iostream>
#include <utility>
#include <string>

int main() {
    const std::string source = "cannot be stolen from";

    // std::move gives `const std::string&&`, which the move constructor
    // (taking `std::string&&`) cannot accept. The copy constructor takes
    // `const std::string&`, which it can. So this copies.
    std::string dest = std::move(source);

    std::cout << "dest:   " << dest << "\n";
    std::cout << "source: " << source << "\n";     // still intact
    return 0;
}
dest:   cannot be stolen from
source: cannot be stolen from

This is a real performance trap. const on a local you were about to move from silently turns your move into a copy.

3. You move a trivial type. Moving an int is copying an int. There is nothing to steal.

Do not move a return value

#include <iostream>
#include <string>
#include <utility>

std::string good() {
    std::string s = "built locally";
    return s;                    // compiler moves (or elides entirely)
}

std::string worse() {
    std::string s = "built locally";
    return std::move(s);         // prevents copy elision — actively harmful
}

int main() {
    std::cout << good() << "\n";
    std::cout << worse() << "\n";
    return 0;
}
built locally
built locally

Both print the same thing, but worse() is worse. Returning a local by name lets the compiler construct it directly in the caller's storage — copy elision, no copy and no move. Wrapping it in std::move turns the return into an rvalue expression and blocks that optimisation, forcing an actual move.

GCC warns about it:

warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]

The rule is simple: return s;, never return std::move(s);.

When to reach for it

Use std::move when you have a named object you are finished with, and want to hand its contents somewhere else:

#include <iostream>
#include <string>
#include <utility>
#include <vector>

int main() {
    std::vector<std::string> collected;

    std::string line = "a line of text we are done with";
    collected.push_back(std::move(line));      // hand it over rather than copy

    std::cout << "stored: " << collected[0] << "\n";
    std::cout << "line is now empty: " << line.empty() << "\n";

    // `line` is valid — reassigning it is fine.
    line = "reused";
    std::cout << "reused: " << line << "\n";
    return 0;
}
stored: a line of text we are done with
line is now empty: 1
reused: reused

Note the last two lines: after being moved from, line was safely assigned a new value. That is what "valid but unspecified" allows.

Try it

Write a class that prints in its copy and move constructors. Then call std::move on: a normal object, a const object, and a function's return value. Predict which prints "move" before running it.

Checkpoint

  • std::move moves nothing — it is a cast to an rvalue reference.
  • It only enables overload resolution to select a move operation; that operation does the work.
  • If the type has no move operations, it silently copies.
  • If the source is const, it silently copies — a common performance trap.
  • Never write return std::move(local); — it blocks copy elision (-Wpessimizing-move).
  • Use it on a named object whose contents you are finished with.

Questions about this lesson

Log in to ask a question.