Quant developer HFT course · Module 2: Safe C++
Move semantics without the mystery
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.
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::moveon aconstobject does not move.const T&&does not bind to the move constructor'sT&¶meter, so the copy constructor is chosen, silently.std::moveon 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?
std::move is a cast. The work is in the move constructor that the cast makes eligible. a remains an object in a valid but unspecified state.
Question 2 of 3
Why might a vector copy, rather than move, its elements when it reallocates?
Through move_if_noexcept, the vector moves only when that cannot throw, or when copying is impossible. A missing noexcept on a hand-written move constructor costs a deep copy per element on every growth.
Question 3 of 3
What is wrong with return std::move(local); in a function returning the local's type?
A returned local is already treated as an rvalue if it is not elided. Wrapping it in std::move changes the expression's type, so the elision rules no longer apply.
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.