---
title: "C++"
book: "The Interview Book"
subject: quant
language: en
chapter: 22
exercises: 0
source: https://one-course.com/books/quant/18/en/chapter/22-c
---

# Chapter 22 — C++

“What does this print?” Eighteen lines: a base class whose constructor calls a virtual function, a derived class that overrides it, one object. The candidate answers “Derived”, explains dynamic dispatch well, and is wrong: while the base’s constructor runs, the object is still a base, and the base’s version is called. C++ interviews at trading firms test the language as the machine sees it: when objects begin and end, what a move really moves, what the compiler may assume, and what the standard library does inside. The performance side is One Quant Book 13’s (chapters 6 to 8); this chapter is the interview side. Every snippet printed here is compiled and run by the chapter’s tests, and every claim of undefined behaviour is diagnosed by a sanitizer, never “tested” for an output.

## 22.1 Object lifetime

**Definition 22.1 (Output-prediction question).**

An *output-prediction question* shows a short program and asks what it prints. It tests the language’s rules (order of construction and destruction, overload resolution, value categories, conversions) and the candidate’s ability to say when the answer is guaranteed, implementation-defined, unspecified, or not defined at all.

The rules of lifetime that such questions probe are few. Members are constructed in the order they are declared, not the order of the initialiser list, and destroyed in reverse. A temporary lives until the end of the full expression that created it, unless it is bound directly to a local reference, which extends its life. During a constructor or destructor the dynamic type is the class being constructed or destroyed, so virtual calls do not reach derived overrides. Objects with static storage in one translation unit are initialised in the order of their definitions; across translation units the order is unspecified.

**Method 22.2 (Answering an output-prediction question).**

1. Walk the program statement by statement, tracking each object’s birth and death.
2. At each call, apply overload resolution with the argument’s value category, not its declared type.
3. Classify every doubtful line: guaranteed by the standard, implementation-defined (sizes, representations), unspecified (the state of a moved-from standard container), or undefined (then say so and stop predicting).
4. Say what a compiler with warnings on would flag.

## 22.2 Moves, copies and value categories

A class that owns a resource either owns it through members that manage themselves (a `std::vector`, a `std::unique_ptr`) and declares no special members at all, the rule of zero, or manages it directly and must define all five: destructor, copy constructor, copy assignment, move constructor, move assignment. Moves should be `noexcept`, or standard containers will copy instead of moving when they grow. A moved-from standard object is valid but in an unspecified state: it may be destroyed or assigned, and nothing else may be assumed. A named rvalue reference is itself an lvalue: inside a function taking `std::string&& s`, passing `s` on selects the lvalue overload unless `std::move(s)` is written.

**Example 22.3 (Which overload?).**

[Listing 22.1](#lst-iv-cpp-overload) calls `f` with a named string, a moved string, a temporary, and a named rvalue reference inside `g`. The last one is the question’s point: the parameter has a name, so it is an lvalue and the `const std::string&` overload is chosen.

```cpp
#include <cstdio>
#include <string>
#include <utility>

void f(const std::string&) { std::printf("lvalue\n"); }
void f(std::string&&) { std::printf("rvalue\n"); }

void g(std::string&& s) { f(s); }

int main() {
    std::string a = "x";
    f(a);
    f(std::move(a));
    f(std::string("y"));
    g(std::string("z"));
}
```

***Listing 22.1.** Overload resolution by value category. code/interviews/22-cpp/cpp/snippets/overload_rvalue.cpp*

## 22.3 Templates and compile-time code

Templates are resolved at compile time and cost nothing at run time when used for static polymorphism (the curiously recurring template pattern of One Quant Book 13, chapter 7). `constexpr` functions can run at compile time, and a constant expression may not contain undefined behaviour, so the compiler rejects it: a table of powers of ten that multiplies once too often fails to compile rather than overflowing silently ([Interview question 22.13](#iq-iv-cpp-13)). Concepts state requirements on template arguments in the signature, so that a wrong type fails at the call with a readable message.

## 22.4 Undefined behaviour and the standard library’s insides

Undefined behaviour (One Quant Book 13, chapter 8) is not “it crashes”: the compiler may assume it never happens and optimise accordingly, so a program with it can do anything, including what the author expected until the next compiler upgrade. The commonest cases in interviews: signed integer overflow, dangling references and views, iterators or references invalidated by a container’s reallocation, reading uninitialised values, and data races. The practical defence is the sanitizers: AddressSanitizer finds use after free and out-of-bounds access, UndefinedBehaviorSanitizer finds overflow and bad shifts, ThreadSanitizer finds races.

Three library facts recur. `std::vector` grows geometrically, so `push_back` is amortised constant time and reallocates occasionally, invalidating every pointer, reference and iterator into it; `reserve` avoids it when the size is known. `std::unordered_map` is a table of buckets with linked nodes, one allocation per element, which is why a flat open-addressing table is faster on a hot path. `std::function` erases the type of any callable and stores it in a small internal buffer or on the heap when it does not fit, which costs an allocation and an indirect call.

## 22.5 Question bank

```cpp
#include <cstdio>

struct Base {
    Base() { std::printf("%s\n", name()); }
    virtual ~Base() = default;
    virtual const char* name() const { return "Base"; }
};

struct Derived : Base {
    Derived() { std::printf("%s\n", name()); }
    const char* name() const override { return "Derived"; }
};

int main() {
    Derived d;
    const Base& b = d;
    std::printf("%s\n", b.name());
}
```

***Listing 22.2.** A virtual call during construction. code/interviews/22-cpp/cpp/snippets/virtual_in_ctor.cpp*

**Interview question 22.1 ★ developer • market maker.**

What does [Listing 22.2](#lst-iv-cpp-virtual) print, and why?

**Solution of Interview question 22.1.**

`Base`, then `Derived`, then `Derived`. While `Base`’s constructor runs, the `Derived` part does not exist yet and the object’s dynamic type is `Base`, so the virtual call resolves to `Base::name`; in `Derived`’s constructor and afterwards through the reference it resolves to `Derived::name`. The same holds in destructors, in reverse. The chapter’s test compiles and runs the snippet.

*What the interviewer is looking for: the dynamic type during construction, and the rule’s mirror in destruction.*

```cpp
#include <cstdio>

struct Noisy {
    const char* tag;
    explicit Noisy(const char* t) : tag(t) { std::printf("make %s\n", tag); }
    ~Noisy() { std::printf("drop %s\n", tag); }
};

struct Pair {
    Noisy second{"second"};
    Noisy first{"first"};
    Pair() { std::printf("body\n"); }
};

int use(const Noisy& n) { return n.tag[0]; }

int main() {
    Pair p;
    int c = use(Noisy{"temp"});
    std::printf("after %c\n", c);
}
```

***Listing 22.3.** Members and a temporary. code/interviews/22-cpp/cpp/snippets/lifetime_order.cpp*

**Interview question 22.2 ★ developer • proprietary firm.**

What does [Listing 22.3](#lst-iv-cpp-lifetime) print, line by line?

**Solution of Interview question 22.2.**

`make second`, `make first` (members in declaration order), `body`, `make temp`, `drop temp` (the temporary dies at the end of the full expression, after `use` returns), `after t`, then at the end of `main` `drop first`, `drop second` (reverse order of construction).

*What the interviewer is looking for: declaration order, temporary lifetime to the end of the full expression, reverse destruction.*

**Interview question 22.3 ★ developer • any.**

A class owns a raw array allocated with `new[]`. Which special member functions must it define, and how could you avoid defining any?

**Solution of Interview question 22.3.**

All five: destructor (`delete[]`), copy constructor and copy assignment (deep copy, or delete them), move constructor and move assignment (steal the pointer, leave the source empty, `noexcept`). Better, the rule of zero: hold a `std::vector<T>` or a `std::unique_ptr<T[]>` and declare none of them; the compiler generates correct ones.

*What the interviewer is looking for: the rule of five and the preference for the rule of zero.*

**Interview question 22.4 ★ developer • proprietary firm.**

What is RAII? Give two examples from a trading system.

**Solution of Interview question 22.4.**

Resource acquisition is initialisation: a resource is owned by an object whose constructor acquires it and whose destructor releases it, so release happens on every exit path, exceptions included. Examples: a `std::lock_guard` around a shared book structure; a session object that logs out and closes the socket in its destructor; a memory-mapped file or a pinned memory region released on scope exit.

*What the interviewer is looking for: the definition in terms of lifetime and exception safety, with concrete examples.*

```cpp
#include <cstdio>
#include <functional>
#include <memory>

int main() {
    std::printf("%zu %zu %zu\n", sizeof(std::unique_ptr<int>), sizeof(std::shared_ptr<int>),
                sizeof(std::function<void()>));
}
```

***Listing 22.4.** Sizes of three library types. code/interviews/22-cpp/cpp/snippets/smart_sizes.cpp*

**Interview question 22.5 ★ developer • market maker.**

Compare `std::unique_ptr` and `std::shared_ptr`: what does each cost, in memory and time? What does [Listing 22.4](#lst-iv-cpp-sizes) print on x86-64 Linux with libstdc++, and which parts of that answer are guaranteed?

**Solution of Interview question 22.5.**

`unique_ptr` with the default deleter is one pointer, and moving it costs a pointer copy. `shared_ptr` is two pointers (object and control block) plus a separately allocated control block (unless made with `make_shared`), and every copy and destruction updates an atomic reference count, which is contended across threads. The snippet prints `8 16 32`: those are libstdc++ sizes on x86-64 (implementation-defined); the standard guarantees only the interfaces. Use `unique_ptr` unless ownership is genuinely shared.

*What the interviewer is looking for: the costs, and the distinction between guaranteed and implementation-defined facts.*

**Interview question 22.6 ★★ developer • proprietary firm.**

What does [Listing 22.1](#lst-iv-cpp-overload) print? Change one line so that `g` forwards its argument as an rvalue.

**Solution of Interview question 22.6.**

`lvalue`, `rvalue`, `rvalue`, `lvalue`. Inside `g`, `s` has a name and so is an lvalue even though its type is an rvalue reference. Write `f(std::move(s))` (or `std::forward` in a template taking a forwarding reference).

*What the interviewer is looking for: value category of a named rvalue reference, and `std::move`.*

```cpp
#include <cstdio>
#include <string>
#include <string_view>

std::string symbol() { return std::string("A_VERY_LONG_INSTRUMENT_SYMBOL_THAT_DEFEATS_SSO"); }

int main() {
    std::string_view s = symbol();  // the temporary string is destroyed at the end of this line
    std::printf("%c\n", s[0]);
}
```

***Listing 22.5.** A view of a symbol. code/interviews/22-cpp/cpp/snippets/ub_dangling_view.cpp*

**Interview question 22.7 ★★ developer • market maker.**

What is wrong with [Listing 22.5](#lst-iv-cpp-view)? How would you find the bug if you did not see it?

**Solution of Interview question 22.7.**

`symbol()` returns a temporary `std::string` that is destroyed at the end of the declaration, so the `string_view` dangles and reading `s[0]` is undefined behaviour. (A short string might sit in the string’s internal buffer and appear to work, which hides the bug; the snippet uses a long one.) AddressSanitizer reports a heap-use-after-free, as the chapter’s test asserts. Fix: keep a `std::string` (`auto s = symbol();`). To find such bugs: build tests with sanitizers, and enable the compiler’s dangling warnings where available.

*What the interviewer is looking for: temporary lifetime and views, and sanitizers as the detection method.*

```cpp
#include <climits>
#include <cstdio>

int main(int argc, char**) {
    int qty = INT_MAX - 1 + argc;  // INT_MAX when run without arguments
    int doubled = qty * 2;         // signed overflow
    std::printf("%d\n", doubled);
}
```

***Listing 22.6.** Doubling a quantity. code/interviews/22-cpp/cpp/snippets/ub_signed_overflow.cpp*

**Interview question 22.8 ★★ developer • proprietary firm.**

What does [Listing 22.6](#lst-iv-cpp-overflow) print? What may an optimising compiler assume about a loop whose bound is `i + 1 > i`?

**Solution of Interview question 22.8.**

Nothing can be predicted: `INT_MAX * 2` overflows a signed `int`, which is undefined behaviour; the chapter’s test asserts only that UndefinedBehaviorSanitizer reports “signed integer overflow”. Because overflow cannot happen in a valid program, the compiler may assume `i + 1 > i` is always true and turn a loop bounded by it into an infinite loop, or remove an overflow check written as `if (a + b < a)`. Use unsigned arithmetic when wrap-around is wanted, a wider type, or checked arithmetic (`__builtin_add_overflow`).

*What the interviewer is looking for: UB stated, not predicted, and what the optimiser does with it.*

```cpp
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> fills;
    fills.push_back(1);
    int& first = fills.front();
    for (int i = 0; i < 100; ++i) fills.push_back(i);  // reallocates: `first` dangles
    std::printf("%d\n", first);
}
```

***Listing 22.7.** Keeping a reference to the first fill. code/interviews/22-cpp/cpp/snippets/ub_invalidated.cpp*

**Interview question 22.9 ★★ developer • market maker.**

Find the bug in [Listing 22.7](#lst-iv-cpp-invalidated) and give two ways to fix it.

**Solution of Interview question 22.9.**

`first` refers into the vector’s buffer; the `push_back`s reallocate it and the reference dangles, so reading it is undefined behaviour (AddressSanitizer: heap-use-after-free). Fixes: `reserve(101)` before taking the reference, keep an index instead of a reference, or copy the value.

*What the interviewer is looking for: reallocation invalidating references, and index-based or reserved alternatives.*

```cpp
#include <cstdio>

int main() {
    int balance = -1;
    unsigned limit = 1;
    if (balance < limit) std::printf("within limit\n");
    else std::printf("over limit\n");
    std::printf("%u\n", static_cast<unsigned>(balance));
}
```

***Listing 22.8.** A limit check. code/interviews/22-cpp/cpp/snippets/sign_compare.cpp*

**Interview question 22.10 ★★ developer, risk • bank.**

What does [Listing 22.8](#lst-iv-cpp-sign) print, and why is it a risk-control bug?

**Solution of Interview question 22.10.**

It prints `over limit` and `4294967295`. In `balance < limit` the `int` is converted to `unsigned`, so $-1$ becomes $2^{32} - 1$ on this platform (32-bit `unsigned`, implementation-defined width) and the comparison is false. A negative balance is reported as over a limit of 1, and the opposite mistake would let a breach through. The compiler warns (`-Wsign-compare`); build with `-Werror`, and keep money and quantities in one signed type.

*What the interviewer is looking for: the usual arithmetic conversions and their consequence for a control.*

**Interview question 22.11 ★★★ developer • market maker.**

Write the move constructor and move assignment operator of a class that owns a heap array of `int64_t` and its size. Why must they be `noexcept`? What state does a moved-from `std::vector` have?

**Solution of Interview question 22.11.**

```cpp
// A buffer that owns raw memory: the rule of five, with moves that leave the source empty.
class Buffer {
public:
    explicit Buffer(std::size_t n) : size_(n), data_(n ? new std::int64_t[n]() : nullptr) {}
    ~Buffer() { delete[] data_; }
    Buffer(const Buffer& o) : Buffer(o.size_) { std::copy(o.data_, o.data_ + size_, data_); }
    Buffer& operator=(const Buffer& o) {
        if (this != &o) {
            Buffer tmp(o);  // copy-and-swap: strong exception guarantee
            swap(tmp);
        }
        return *this;
    }
    Buffer(Buffer&& o) noexcept
        : size_(std::exchange(o.size_, 0)), data_(std::exchange(o.data_, nullptr)) {}
    Buffer& operator=(Buffer&& o) noexcept {
        if (this != &o) {
            delete[] data_;
            size_ = std::exchange(o.size_, 0);
            data_ = std::exchange(o.data_, nullptr);
        }
        return *this;
    }
    void swap(Buffer& o) noexcept {
        std::swap(size_, o.size_);
        std::swap(data_, o.data_);
    }
    std::size_t size() const { return size_; }
    std::int64_t& operator[](std::size_t i) { return data_[i]; }

private:
    std::size_t size_;
    std::int64_t* data_;
};
```

*A buffer with the rule of five; the moves leave the source empty and cannot throw. code/interviews/22-cpp/cpp/iv_cpp.hpp*

Moves must be `noexcept` so that `std::vector<Buffer>` moves elements when it grows (it copies if a move could throw, to keep its strong guarantee). A moved-from `std::vector` is valid but unspecified; libstdc++ leaves it empty (the chapter’s snippet prints `v=0`), but portable code only destroys or reassigns it. The chapter’s C++ test checks moves, copies and self-assignment.

*What the interviewer is looking for: `std::exchange`-style moves, `noexcept` and why, and the moved-from contract.*

**Interview question 22.12 ★★★ developer • proprietary firm.**

Write a fixed-capacity, single-threaded ring buffer for a hot path. What happens when it is full, and why a power-of-two capacity?

**Solution of Interview question 22.12.**

```cpp
// Single-threaded fixed-capacity ring buffer; N is a power of two: indices wrap with a mask.
template <typename T, std::size_t N>
    requires(N > 0 && (N & (N - 1)) == 0)
class RingBuffer {
public:
    bool push(const T& v) {
        if (tail_ - head_ == N) return false;  // full: the caller decides (drop, block, grow)
        buf_[tail_++ & (N - 1)] = v;
        return true;
    }
    std::optional<T> pop() {
        if (head_ == tail_) return std::nullopt;
        return buf_[head_++ & (N - 1)];
    }
    std::size_t size() const { return tail_ - head_; }

private:
    std::array<T, N> buf_{};
    std::size_t head_ = 0, tail_ = 0;  // unsigned counters: wrap-around is well defined
};
```

*A fixed-capacity ring buffer with monotonically increasing unsigned counters. code/interviews/22-cpp/cpp/iv_cpp.hpp*

When full, `push` returns false and the caller chooses a policy: drop, block, or signal back-pressure (One Quant Book 13, chapter 12). A power-of-two capacity replaces the modulo by a mask, and free-running unsigned counters make “full” and “empty” distinguishable without wasting a slot. The test compares it with a `std::deque` model over 100 000 random operations.

*What the interviewer is looking for: the full/empty logic, the mask, and a stated full policy.*

**Interview question 22.13 ★★★ developer, mle • market maker.**

Write a compile-time table of the powers of ten that fit in an `int64_t`, and a concept that accepts any type with integer `price` and `qty` members. What happens if your table’s loop computes $10^{19}$?

**Solution of Interview question 22.13.**

```cpp
// Compile-time table of powers of ten for integer price scaling.
constexpr std::array<std::int64_t, 19> pow10_table() {
    std::array<std::int64_t, 19> t{};
    t[0] = 1;
    for (std::size_t i = 1; i < t.size(); ++i) t[i] = t[i - 1] * 10;  // stops at 10^18
    return t;
}
inline constexpr auto kPow10 = pow10_table();

// A concept for anything that looks like an order: an integer price and quantity.
template <typename O>
concept OrderLike = requires(const O& o) {
    { o.price } -> std::convertible_to<std::int64_t>;
    { o.qty } -> std::convertible_to<std::int64_t>;
};

template <OrderLike O>
constexpr std::int64_t notional(const O& o) {
    return o.price * o.qty;
}
```

*A constexpr table and a concept. code/interviews/22-cpp/cpp/iv_cpp.hpp*

If the loop multiplies once more, to $10^{19}$, the computation overflows `int64_t`; in a constant expression undefined behaviour is not allowed, so the compiler refuses to compile it (g++ reports “overflow in constant expression”). That happened while writing this chapter, and it is the argument for computing tables at compile time. The concept makes `notional` accept any type with integer `price` and `qty` and reject others with a clear error; the test checks both with `static_assert`.

*What the interviewer is looking for: constexpr evaluation catching UB, and concepts as checked interfaces.*

```cpp
#include <cstdio>

int trace(const char* name, int v) {
    std::printf("init %s\n", name);
    return v;
}

int b = trace("b", 2);
int a = trace("a", b + 1);

int main() { std::printf("a=%d b=%d\n", a, b); }
```

***Listing 22.9.** Two global variables. code/interviews/22-cpp/cpp/snippets/static_init.cpp*

**Interview question 22.14 ★★★ developer • any.**

What does [Listing 22.9](#lst-iv-cpp-static) print? What changes if `a` and `b` are defined in different source files, and how do you make the program correct in that case?

**Solution of Interview question 22.14.**

`init b`, `init a`, `a=3 b=2`: within one translation unit, dynamic initialisation follows the order of definition. Across translation units the order is unspecified, so `a` could be initialised before `b` and read zero (static storage is zero-initialised first): the static initialisation order fiasco. Fix: make the dependency a function returning a local static (initialised on first use, thread-safe), or make the values `constexpr`/`constinit` so that they are initialised at compile time.

*What the interviewer is looking for: the within-unit guarantee, the cross-unit fiasco and the standard fixes.*

Sources and further reading

- ISO/IEC 14882:2020, *Programming languages — C++* , and the public working draft N4861 (2020), for the rules of lifetime, overload resolution, constant evaluation and undefined behaviour.
- One Quant Book 13, chapters 6–9 (C++ for latency; undefined behaviour and the compiler; Rust).
