Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Quant developer HFT course · Module 2: Safe C++

Move semantics without the mystery

Lesson · 11 min

The question

What does std::move do? And why can marking a move constructor noexcept make a vector of orders much faster to grow?

What a move is

Copying an object that owns heap memory copies the memory. Moving it transfers the ownership: the new object takes the pointer, and the old one is left empty.

beforeaftersrcdata · size 3 · cap 4dstemptyheap buffer[q0 q1 q2 _]src after movenull · 0 · 0dst after movedata · size 3 · cap 4
Moving a vector copies three pointers and empties the source. The elements never move; the buffer changes owner.
std::vector<Quote> a = load_quotes();   // a owns a buffer of a million quotes
std::vector<Quote> b = a;               // copy: allocate, copy 24 MB
std::vector<Quote> c = std::move(a);    // move: copy three pointers, a is now empty

The move constructor of std::vector copies the buffer pointer, the size and the capacity from the source, then sets the source's pointers to null. It does the same amount of work for one element or a billion. That is the whole point: moving is how C++ avoids deep copies when the source is not needed any more.

What std::move does

Nothing at run time. std::move(x) is a cast to an rvalue reference, static_cast<T&&>(x). It tells overload resolution that x may be treated as a temporary, so the move constructor or move assignment is chosen over the copy. The move itself, if there is one, happens in that constructor.

Two consequences:

  • std::move on a const object does not move. const T&& does not bind to the move constructor's T&& parameter, so the copy constructor is chosen, silently.
  • std::move on a type with no move constructor copies.

After a move, the source is in a valid but unspecified state for standard library types. You may assign to it or destroy it; you should not read its value. Writing std::move(order) and then using order in the next line is a bug that compiles cleanly; clang-tidy's bugprone-use-after-move check finds it.

noexcept and vector growth

When a vector grows it allocates a larger buffer and transfers the elements across. It wants to move them. But the standard requires push_back to give the strong exception guarantee: if anything throws, the vector is left as it was. If a move throws half way through, some elements have already left the old buffer and the original state cannot be restored.

So std::vector moves elements only if the move constructor is noexcept (or the type cannot be copied at all). Otherwise it copies them, which can be expensive for elements that own memory. The decision is made through std::move_if_noexcept.

struct Order {
    std::string tag;
    std::vector<Fill> fills;
    Order(Order&&) noexcept = default;          // explicitly noexcept: vector will move
    Order& operator=(Order&&) noexcept = default;
};

Defaulted move constructors are noexcept automatically when all members' moves are. A user-written one is not, unless you say so. A hand-written move constructor without noexcept turns every reallocation into a deep copy. You can check with static_assert(std::is_nothrow_move_constructible_v<Order>).

Copy elision

Often there is no move at all. When a function returns a local object, or a temporary initialises an object of the same type, the compiler constructs the result directly in the destination.

Book build_book(const Snapshot& s) {
    Book b;
    b.load(s);
    return b;              // named return value optimisation: usually no move, no copy
}
Book bk = build_book(snap);  // C++17: initialising from a prvalue is guaranteed elision

Since C++17, initialising from a prvalue such as build_book(snap) is guaranteed to construct in place. Returning a named local is permitted to be elided and in practice almost always is; if not, the local is moved, because return statements treat locals as rvalues.

This is why return std::move(b); is a mistake: it turns the local into an rvalue reference expression, which disables the elision and forces a move.

Taking parameters for storage

A function that stores its argument can take it by value and move it into place:

class Strategy {
    std::string name_;
public:
    explicit Strategy(std::string name) : name_(std::move(name)) {}
};
Strategy s1("momentum");        // constructs the parameter from the literal, then moves
Strategy s2(config_name);       // one copy into the parameter, then a move

Callers with temporaries pay a move; callers with lvalues pay one copy, the minimum possible. One function serves both.

Check yourself

Question 1 of 3

What happens at run time when you write auto b = std::move(a); for a std::string a?

Question 2 of 3

Why might a vector copy, rather than move, its elements when it reallocates?

Question 3 of 3

What is wrong with return std::move(local); in a function returning the local's type?

In the interview

Say your own answer out loud first, then open the one we would give.

What does std::move do?

It is a cast to an rvalue reference. It does not move anything; it makes the object eligible to bind to a move constructor or move assignment, which then transfers ownership. On a const object, or a type without move operations, the copy is used instead.

Why should move constructors be noexcept?

Containers such as vector offer the strong exception guarantee when they grow. They can only move elements if the move cannot throw; otherwise they copy. A move constructor without noexcept turns every reallocation into deep copies.

What state is an object in after it has been moved from?

For standard library types, valid but unspecified: it can be destroyed or assigned a new value, but you should not depend on what it contains. In my own types I would leave it empty and document that.

What is copy elision?

The compiler constructing a returned or temporary object directly in its final location, so no copy or move happens. Since C++17 it is guaranteed for initialisation from a prvalue; for a named local it is permitted and almost always done, with a move as the fallback.