Facebook Pixel

Copies, Moves, and Object Layout

The update loop calls on_update(update) with an element from its vector. The parameter declaration decides whether that call creates another MarketUpdate:

void on_update(MarketUpdate u);          // by value
void on_update(const MarketUpdate& u);   // by reference

MarketUpdate u declares a separate parameter object. Here, update is a named lvalue, so the call copies it. Passing std::move(update) would make a move eligible, while a prvalue could initialize u directly. With const MarketUpdate& u, the parameter refers to the vector element.

By-value creates another MarketUpdate for the parameter. By-reference points back to the caller's existing update.

What a copy does

With MarketUpdate b = a;, changing b.price afterward has no effect on a.price. MarketUpdate stores all of its fields inline, so its implicit copy constructor initializes a second set of those fields. The optimizer may combine or remove that work, but the source asks for an independent object.

Its size comes from the field layout, and adding the declared field sizes gives the wrong answer on the target used in this lesson.

Object layout and padding

The ABI assigns an alignment requirement to each field. The compiler inserts padding so fields start at suitable offsets and consecutive structs remain aligned in an array. On the machine used for the diagram, the layout is 32 bytes. Check sizeof again when compiling for a different target or ABI.

MarketUpdate's 32-byte memory layout as a horizontal bar: sequence (8 bytes), symbol[8] (8 bytes), price (8 bytes), qty (4 bytes), then 4 bytes of dashed padding. A byte scale below marks 0, 8, 16, 24, 28, 32. Labeled sizeof(MarketUpdate) == 32.
The dashed final four bytes keep the next array element aligned.

Here, qty ends at byte 28. Four bytes of tail padding bring sizeof(MarketUpdate) to 32, which is the size used in the cache-line article.

A C++ playground showing the MarketUpdate struct: uint64_t sequence (8 bytes), char symbol[8] (8 bytes), int64_t price (8 bytes), int32_t qty (4 bytes), and a main() that prints sizeof(MarketUpdate). The program output reads: sizeof(MarketUpdate): 32 bytes.

When a copy gets expensive

sizeof becomes less informative once an object owns dynamic storage. This Order records each completed piece of the order as a fill:

struct Order {
    std::uint64_t      id;      // 8 bytes
    std::vector<Fill>  fills;   // a small inline header...
};

The fills vector keeps its bookkeeping inside Order and its elements in a separate buffer. At this point, sizeof(Order) is the wrong number to use for copy cost: it stays the same whether the vector owns three fills or 1,000.

// Order a, holding 3 fills:
Order a;
//  a.id             = 7
//  a.fills.size     = 3
//  a.fills.capacity = 3
//  a.fills.data     = 0x100   // heap buffer holding f0, f1, f2

Order b = a; reaches the allocator through fills. Its copy constructor reserves storage for b.fills, then copy-constructs the elements from a.fills. Each order ends up with its own buffer. Copying the outer object and the dynamic data it owns is a deep copy.

Copying an Order gives b a new fills buffer at 0x200 and copies the three elements from a's buffer at 0x100.

Change the order from three fills to 1,000 and Order b = a; copy-constructs 1,000 elements. sizeof(Order) stays unchanged because it excludes the buffer.

Move: transferring instead of duplicating

Sometimes you need both orders afterward, and the copy is correct. If a is finished and b is taking over, a second fills buffer would be discarded with a soon afterward.

Order b = std::move(a); moves a.fills into b.fills. With ordinary vector move construction, b.fills takes over the buffer at 0x100; the three Fill objects stay where they are.

During Order b = std::move(a), the pointer to the buffer at 0x100 moves from a to b. The illustration resets a's vector fields to zero and leaves the three Fill objects in place.

The moved-from a is valid, so destroying it or assigning a new value is safe. Its precise state is unspecified; do not rely on a.fills.empty() unless the type documents that result. Ordinary vector move construction transfers the buffer in constant time regardless of the number of fills.

How the compiler chooses, and what std::move really does

The name a is an lvalue, so Order b = a; normally selects the copy constructor. std::move(a) casts it to an xvalue, which can bind to an rvalue reference and make the move constructor eligible. The constructor performs the transfer. Types without a usable move constructor still copy, and const can also prevent the expected move.

Module 2 covers value categories and reference binding in detail. The value category reference lists the complete language rules.

Constructor-selection paths for a named lvalue, a returned value, and a named object cast with std::move.
Order a = make_order();

Order b = a;              // a is a named lvalue (still usable) -> copy (deep copy)
Order c = make_order();   // since C++17, the returned prvalue can construct c directly
Order d = std::move(a);   // makes a eligible for move construction

Order c = make_order(); often involves neither constructor shown in the diagram. Since C++17, a returned Order prvalue can construct c directly. A named local such as tmp is eligible for named return-value optimization (NRVO); without NRVO, the return can move it.

Order make_order() {
    Order tmp;
    // ... fill tmp ...
    return tmp;           // NRVO may construct tmp directly as the result
}

Leave return tmp; as written because std::move(tmp) can prevent NRVO. Elsewhere, use std::move when the source object can safely enter a moved-from state.

Range-for declarations copy too

A range-for declaration also decides whether each element is copied:

// Deep-copies each Order: header plus a fresh fills buffer, every iteration.
for (Order o : orders) {
    apply(o);
}

// Binds to each existing Order without copying it.
for (const Order& o : orders) {
    apply(o);
}

Read the declaration as you would a function parameter. Order o constructs a new object from every element. const Order& o binds to the element already in the vector. This loop only reads each order, so the reference expresses that intent and avoids the deep copy.

Checkpoint

1)

Order owns a std::vector<Fill> currently holding 1,000 fills. What does Order b = std::move(a); do?

In interviews

  • "Is passing a struct by value expensive?" → "I would check its size and what its copy constructor does. A flat struct copies its inline fields. A struct containing a vector may allocate another element buffer and copy every element."
  • "What does std::move actually do?" → "It casts its argument to an xvalue so a move overload can be selected. The move constructor or move assignment operator performs the transfer."
  • "Why is sizeof bigger than the sum of the fields?" → "The compiler inserts padding to satisfy field and object alignment. On this target, MarketUpdate has 28 bytes of fields and four bytes of tail padding."
  • "Spot the cost: for (Order o : orders)." → "Each iteration copy-constructs o from an element. If the loop only reads the order, I would bind const Order& instead."