Skip to content

Step 1 of 7 · Reading · ~4 min

Learn

Modern C++ Patterns

Move Semantics and Rvalue References

You will be able to write the move constructor and move assignment that finish the Rule of Five, say what std::move really does, and read a copy-and-move counter.

You have written std::move(p) since smart pointers and wrote the copy half of the Rule of Five last lesson. This lesson explains std::move and writes the other two members.

Two kinds of expression, three kinds of reference

An lvalue has a name and an address (s). An rvalue is a temporary (std::string("x"), a function's return value). T& binds lvalues, const T& both, and the new T&& only rvalues. Choosing among same-named functions by their arguments is overload resolution, so void f(const std::string&) can run for f(s) and void f(std::string&&) for f(std::string("x")).

std::move is only a cast

std::move(x) moves nothing. It casts x to an rvalue so overload resolution picks the T&& member, which does the stealing. The clearest case is a type that cannot be copied. Handle owns a numeric resource id, with -1 meaning "owns nothing":

class Handle {
public:
    explicit Handle(int id) : id_(id) {}
    ~Handle() { close_(); }
    Handle(const Handle&) = delete;
    Handle& operator=(const Handle&) = delete;
    Handle(Handle&& o) noexcept : id_(o.id_) { o.id_ = -1; }
    Handle& operator=(Handle&& o) noexcept {
        if (this != &o) { close_(); id_ = o.id_; o.id_ = -1; }
        return *this;
    }
private:
    void close_() { if (id_ >= 0) std::cout << "close " << id_ << "\n"; id_ = -1; }
    int id_;
};

The move constructor takes the resource and resets the source, so its destructor finds nothing to release. The move assignment first releases what the target owns. Trace it:

Handle a(7);
Handle b = std::move(a);   // b owns 7, a owns nothing
Handle c(9);
c = std::move(b);          // prints "close 9", then c owns 7
// scope ends: c prints "close 7"; a and b print nothing

Handle d = a; is a compile error; the cast is the only way to hand a over.

The moved-from a is valid but empty. For your own class you define that state (-1 here); for library types it is unspecified, so assign to it or destroy it, never read it. std::move on a const object gives a const T&&, which cannot bind to T&&, so const std::string k = "abc"; std::string d = std::move(k); quietly copies.

Where the compiler moves for you

A returned value and a temporary argument are already rvalues, and in return h; the compiler treats the local h as one too. That is why a function can return a Handle by name. Copy elision means the compiler builds the object where the caller wants it and skips the copy or move. return h; allows that; return std::move(h); hides it and forces a real move.

noexcept decides how a vector grows

noexcept promises a function will not throw. When a std::vector outgrows its block it must relocate (new bigger block, transfer each element, free the old one). A failed copy leaves the original intact, but a move that throws has already damaged its source, so the vector moves only if the move constructor is noexcept, and otherwise copies. Count with a bare struct:

static int n_copy = 0, n_move = 0;
struct Tok {
    Tok() {}  Tok(const Tok&) { ++n_copy; }
    Tok(Tok&&) noexcept { ++n_move; }
};
std::vector<Tok> v;
for (int i = 0; i < 4; ++i) v.push_back(Tok());

With noexcept this costs 0 copies and 7 moves: four pushes plus three relocations (capacity 1 to 2 moves one element, 2 to 4 moves two). Delete noexcept and it costs 3 copies and 4 moves; nothing warns you.

The sink parameter idiom takes by value, then moves in: explicit Account(Tok p) : tag(std::move(p)) {}. A named Tok t costs 1 copy and 1 move; std::move(t) costs 0 and 2. std::swap(a, b) is three moves, or three copies without move members.

Common mistakes

  • Not resetting the source. Two objects release one resource: close 7 prints twice, or a heap block is freed twice and the program aborts.
  • Leaving out noexcept. Every value is right, but vector growth copies, so the copy counter is above 0.
  • Forgetting std::move at a call site. to = from; copies even when move members exist.
  • Skipping the release in move assignment. What the target owned before is leaked.

Your exercise

Give a copy-only IntBuffer its move members and fix the two call sites that still copy; the counters show which ran.

Up nextTemplates Beyond the Basics: Deduction, Forwarding and SpecializationTemplates and STL Algorithms

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…