量化金融 · 第 20 册 · 职业发展

Interviews III: Code

本书仅提供英文版。

Interviews III: Code · 职业发展

3C++

Lifetime, ownership and undefined behaviour first, then writing the standard library’s own classes on a whiteboard: a smart pointer, a vector, an allocator. Stems say what they compile with where the answer depends on it, which here is C++20 on g++ 11.4, libstdc++, x86-64 Linux. Learn what the language promises separately from what this compiler happens to do.

Question 3.1 ★ QD, Dev, LLE • any • junior

In Listing 3.1, replace the comment with each of these lines in turn: *p = 3;, p = &b;, *q = 3;, q = &b;, *r = 3;, r = &b;. Which ones compile? How do you read such a declaration?

int main() {
    int a = 1, b = 2;
    const int* p = &a;
    int* const q = &a;
    const int* const r = &a;
    (void)b, (void)p, (void)q, (void)r;
    // try each line here
}
Listing 3.1. Three const pointers. code/interviews-3/03-cpp/cpp/ptr_const.cpp
Answer

Answer to Question 3.1.

p = &b and *q = 3 compile, and the other four do not. Read a declaration from the name outward, to the left: p is a pointer to a const int, and q is a const pointer to an int.

So p can move to another object and cannot write through itself. q can write and cannot move, and r can do neither. a itself is not constant, so a read through p can still see a change made through a or q: const int* promises only that this path will not write.

const int* and int const* are the same type, since the position of const relative to int does not matter. Only its position relative to the * does.

Classic wrong answer. “const int* p means the value cannot change.” It only stops writes through p.

Follow-up. And in a function signature? void f(int* const p) has the same type as void f(int* p): the top-level const applies to the function’s own copy of the argument, so it does not change the caller’s view.

Question 3.2 ★ QD, Dev, LLE • any • junior

What does Listing 3.2 print, in order? Which object is never destroyed? Name the storage duration of each of the four objects, and say where each one lives on x86-64 Linux.

#include <iostream>

struct Noisy {
    const char* name;
    explicit Noisy(const char* n) : name(n) { std::cout << "+" << name << " "; }
    ~Noisy() { std::cout << "-" << name << " "; }
};

Noisy global("global");

void f() {
    static Noisy s("static");
    Noisy a("auto");
}

int main() {
    f();
    f();
    Noisy* h = new Noisy("heap");
    (void)h;
    std::cout << "end ";
}
Listing 3.2. Four kinds of lifetime. code/interviews-3/03-cpp/cpp/storage.cpp
Answer

Answer to Question 3.2.

It prints +global +static +auto -auto +auto -auto +heap end -static -global. The heap object is never destroyed, since nothing calls delete, so -heap never appears.

global has static storage duration, and is built before main. s also has static storage duration, but is built the first time control passes through its declaration, which is why +static appears once. a has automatic storage, is built and destroyed on each call, and lives on the stack. *h has dynamic storage on the heap, and the pointer h itself is automatic.

At exit, static objects are destroyed in the reverse order of their construction, so s goes before global. The two static objects live in the program’s data segment, not on the stack or the heap.

Classic wrong answer. “The operating system frees the heap object at exit, so its destructor runs.” The memory is reclaimed, but no destructor runs. A destructor that flushes a file would never flush.

Follow-up. Is the static local thread-safe? Since C++11, its initialisation is: a second thread waits until it finishes.

Question 3.3 ★ QD, Dev, LLE • any • junior

In C++, what is the difference between class and struct? In Listing 3.3, replace the comment with each of (void)S{}.x;, (void)C{}.x;, (void)FromC{}.get(); and (void)FromS{}.x;. Which compile?

struct S { int x = 1; };
class C { int x = 1; public: int get() const { return x; } };
struct FromC : C {};
class FromS : S {};

int main() {
    // try each line here
}
Listing 3.3. Default access. code/interviews-3/03-cpp/cpp/class_struct.cpp
Answer

Answer to Question 3.3.

Only default access differs: a struct’s members and bases are public by default, and a class’s are private. S{}.x, C{}.get() and FromC{}.get() compile, while C{}.x and FromS{}.x do not.

FromC is a struct, so it inherits publicly, and get stays public. FromS is a class, so it inherits privately, which hides x even though x is public in S. The default comes from the derived class, not from the base.

Both keywords can have methods, constructors, virtual functions and templates, and can be mixed freely in one hierarchy. Layout does not depend on the keyword.

The common convention is struct for a bundle of public data with no invariant, and class where members are kept private to protect one.

Classic wrong answer. “A struct is plain data, and it lives on the stack.” That is C#’s distinction, not C++’s. Where an object lives depends on how it is created.

Question 3.4 ★ QD, Dev, LLE • any • junior

What two lines does Listing 3.4 print? What does r = b do, and how does that differ from p = &b? Give two other differences between a reference and a pointer.

#include <iostream>

int main() {
    int a = 1, b = 2;
    int& r = a;
    r = b;
    b = 3;
    std::cout << a << " " << b << " " << r << "\n";
    int* p = &a;
    p = &b;
    *p = 4;
    std::cout << a << " " << b << " " << *p << "\n";
}
Listing 3.4. A reference and a pointer. code/interviews-3/03-cpp/cpp/ref_rebind.cpp
Answer

Answer to Question 3.4.

It prints 2 3 2 and 2 4 4. r = b assigns the value of b to a, because r is another name for a, and a reference cannot be reseated. p = &b makes the pointer point elsewhere, and leaves a alone.

So after r = b, a is 2, and the later b = 3 does not touch it. Then *p = 4 writes to b.

Two other differences. A reference must be bound when it is declared, and it is never meant to be null, so a function taking one need not check. And a pointer is an object of its own, which you can take the address of, keep in a container and do arithmetic on, while &r just gives the address of a.

Classic wrong answer. “A reference is safer, as it cannot dangle.” It can. A reference to a local returned from a function dangles just as a pointer does.

Follow-up. When is a pointer the right parameter? When “nothing” is a valid argument, or when the function keeps it and may be pointed elsewhere later.

Question 3.5 ★ QD, Dev, LLE • any • junior

With g++ 11.4 on x86-64, with 4-byte int and 8-byte pointers, what three numbers does Listing 3.5 print? Why does the parameter’s [10] not help? How do you pass an array and keep its length?

#include <iostream>
#include <iterator>

void show(int a[10]) { std::cout << sizeof(a) << "\n"; }

int main() {
    int a[10] = {};
    std::cout << sizeof(a) << "\n";
    show(a);
    std::cout << std::size(a) << "\n";
}
Listing 3.5. An array as a parameter. code/interviews-3/03-cpp/cpp/array_param.cpp
Answer

Answer to Question 3.5.

It prints 40, 8 and 10. Inside main, a is an array of 10 int, and sizeof gives 40 bytes. As a parameter, int a[10] means int* a, so the function sees a pointer, of 8 bytes, and the 10 in the brackets is ignored.

This decay is inherited from C: an array passed to a function turns into a pointer to its first element. The length is not passed at all, so the caller could hand over an array of 3, and the compiler would say nothing. g++ warns on this sizeof by default, which is why the listing has the warning switched off on a line not shown.

To keep the length, take a std::span<int> in C++20, which carries pointer and size and accepts arrays and vectors alike. Or take a reference to the array, int (&a)[10], or use a template over the length. std::size works on a true array because its type carries the length.

Classic wrong answer. “sizeof(a) / sizeof(a[0]) gives the length.” Inside the function it gives 8/4=28 / 4 = 2, whatever was passed.

Follow-up. Why does std::array avoid this? It is a struct holding the array, so it passes by value or reference with its size in the type.

Question 3.6 ★ QD, Dev, LLE • any • junior

With g++ on x86-64, what three lines does Listing 3.6 print? What is the type of a + b? Is any of this undefined?

#include <cstdint>
#include <iostream>

int main() {
    std::uint8_t a = 200, b = 100, e = 65;
    auto c = a + b;
    std::uint8_t d = a + b;
    std::cout << c << " " << sizeof(c) << "\n";
    std::cout << int(d) << "\n";
    std::cout << e << " " << int(e) << " " << +e << "\n";
}
Listing 3.6. Small integers. code/interviews-3/03-cpp/cpp/promote.cpp
Answer

Answer to Question 3.6.

It prints 300 4, then 44, then A 65 65. a + b has type int, and nothing here is undefined.

Types smaller than int are promoted to int before arithmetic, so a + b is the integer 300, and auto makes c a 4-byte int. Storing 300 into uint8_t reduces it modulo 256, which gives 44. That conversion to an unsigned type is defined. -Wall -Wextra does not warn, and nor does -Wconversion alone on g++ 11.4: it takes -Wconversion -Warith-conversion.

std::uint8_t is an alias of unsigned char here, so cout prints it as a character, and 65 appears as A. int(e) or the unary plus, which promotes, prints the number.

The trap in feed parsers is that a one-byte field logged with cout prints as a control character, or as nothing.

Classic wrong answer. “a + b overflows, since both are 8 bits.” The addition happens in int, so there is no overflow.

Follow-up. Can promotion cause undefined behaviour? Yes: two uint16_t values of 65535 multiply as signed int, and that overflows.

Question 3.7 ★ QD, Dev, LLE • any • junior

What two lines does Listing 3.7 print? State the rule that decides between the two meanings of each pair of arguments.

#include <iostream>
#include <vector>

int main() {
    std::vector<int> a(3, 1), b{3, 1}, c(3), d{3};
    std::cout << a.size() << " " << b.size() << " " << c.size() << " " << d.size() << "\n";
    std::cout << a[0] << " " << b[0] << " " << c[0] << " " << d[0] << "\n";
}
Listing 3.7. Parentheses and braces. code/interviews-3/03-cpp/cpp/vector_init.cpp
Answer

Answer to Question 3.7.

It prints 3 2 3 1 and 1 3 0 3. Parentheses choose among constructors as usual, so a(3, 1) is three copies of 1, and c(3) is three zeros. Braces prefer an initializer_list constructor whenever one fits, so b{3, 1} is the two elements 3 and 1, and d{3} is one element.

The rule is that brace initialisation looks at initializer_list constructors first, and considers the others only if none is viable. So for any container of numbers, braces mean “these elements”.

With std::vector<std::string> v{3, "x"}, the list cannot convert, so the constructor taking a count and a value is used, and you get three strings. The same braces thus change meaning with the element type, which matters in templates.

Classic wrong answer. “Braces are the modern way, so use them everywhere.” They also forbid narrowing, which is good, but on a container of numbers they do something other than parentheses.

Follow-up. What is a good habit? Use parentheses for a size and braces for a list of elements, and do not mix them in generic code without thinking.

Question 3.8 ★ QD, Dev, LLE • any • junior

What does Listing 3.8 print? The initialiser list names b first. What decides the order, and which warning would have told you? Why does it matter if b’s initialiser reads a?

#include <iostream>

struct Noisy {
    const char* name;
    explicit Noisy(const char* n) : name(n) { std::cout << "+" << name << " "; }
    ~Noisy() { std::cout << "-" << name << " "; }
};

struct Pair {
    Noisy a;
    Noisy b;
    Pair() : b("b"), a("a") {}
};

int main() {
    Pair p;
    std::cout << "body ";
}
Listing 3.8. The order of members. code/interviews-3/03-cpp/cpp/member_order.cpp
Answer

Answer to Question 3.8.

It prints +a +b body -b -a. Members are initialised in the order they are declared in the class, whatever the order in the initialiser list, and destroyed in the reverse order. -Wreorder, which -Wall includes, reports the mismatch.

The listing turns off that warning on a line not shown, as it would otherwise fail a build with -Werror.

It matters when one initialiser depends on another. With Pair() : b(a.name), a("a"), b is built second, so that one happens to work. The reverse dependency is the bug: if a’s initialiser reads b, it reads an object that does not yet exist, which is undefined, and reading the list suggests otherwise.

The reason for a fixed order is destruction. There may be several constructors with different lists, but only one destructor, and it must undo the construction in reverse.

Classic wrong answer. “The list decides, as it runs left to right.” Base classes come first, and then the members in declaration order.

Follow-up. What of the constructor body? It runs after every member is built, so an assignment in the body is a second write, not an initialisation.

Question 3.9 ★ QD, Dev, LLE • any • junior

What five lines does Listing 3.9 print? What is the type of nullptr? Why was it added to the language, when 0 already meant a null pointer?

#include <iostream>

void f(int) { std::cout << "int\n"; }
void f(char*) { std::cout << "pointer\n"; }

template <class T>
void relay(T x) { f(x); }

int main() {
    f(0);
    f(nullptr);
    char* p = nullptr;
    f(p);
    relay(nullptr);
    relay(0);
}
Listing 3.9. Zero and nullptr. code/interviews-3/03-cpp/cpp/overload_zero.cpp
Answer

Answer to Question 3.9.

It prints int, pointer, pointer, pointer, int. nullptr has type std::nullptr_t, which converts to any pointer type and not to int. The literal 0 is an int, so it picks the int overload.

Before C++11, null was written 0 or NULL, and either could silently choose an overload for integers. A literal 0 also stops being a null pointer once it passes through a template: in relay(0), T is int, and an int variable holding 0 cannot convert to a pointer, so if only the pointer overload existed, relay(0) would not compile. relay(nullptr) keeps the type, and that still converts.

So nullptr says “no object” in the type system, and it survives being passed around. Its value is not the issue: on x86-64 Linux the null pointer is the all-zero address, but no code should depend on that.

Classic wrong answer. “0 and nullptr are the same thing.” In f(0) they would call different functions, which is the whole point.

Question 3.10 ★ QD, Dev, LLE, QR • any • junior

What does Listing 3.10 print? Why does std::remove not change the size of the vector? What is the C++20 one-line way to do both steps?

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{1, 0, 2, 0, 3};
    auto end = std::remove(v.begin(), v.end(), 0);
    std::cout << v.size() << ":";
    for (int x : v) std::cout << " " << x;
    std::cout << "\n";
    v.erase(end, v.end());
    std::cout << v.size() << "\n";
}
Listing 3.10. Removing without erasing. code/interviews-3/03-cpp/cpp/remove_only.cpp
Answer

Answer to Question 3.10.

It prints 5: 1 2 3 0 3 and then 3. std::remove works on iterators, so it can only move elements around. It cannot change a container’s size, as it never sees the container. In C++20, std::erase(v, 0) does both steps.

The kept elements are shifted to the front, and the function returns the new logical end. What lies beyond is valid but unspecified, which is why the tail here reads 0 3: those are the old positions, left as they were. For a type with a move, they may be moved-from.

The classic form is v.erase(std::remove(v.begin(), v.end(), 0), v.end()), which is linear, with each kept element moved at most once. That is far better than erasing one element at a time, which is quadratic.

Classic wrong answer. Using v.end() after std::remove as though it had moved. It has not, so a loop to v.end() sees the leftovers.

Follow-up. What about a std::list? It has its own member remove, which unlinks nodes and does change the size.

Question 3.11 ★ QD, Dev, LLE, QR • any • junior

What two lines does Listing 3.11 print? The code compiles with no warning under -Wall -Wextra. Why is the first number wrong?

#include <iostream>
#include <numeric>
#include <vector>

int main() {
    std::vector<double> fills{0.6, 0.6, 0.6};
    std::cout << std::accumulate(fills.begin(), fills.end(), 0) << "\n";
    std::cout << std::accumulate(fills.begin(), fills.end(), 0.0) << "\n";
}
Listing 3.11. The starting value of a sum. code/interviews-3/03-cpp/cpp/accumulate_int.cpp
Answer

Answer to Question 3.11.

It prints 0 and then 1.8. In accumulate, the type of the running total is the type of the initial value, so a literal 0 makes it an int, and each step truncates.

So the steps are 0+0.6=0.60 + 0.6 = 0.6, stored in an int as 0, three times over. The result is 0, and not even 1 or 2, since nothing ever carries. With fills of 0.6 the total stays 0 forever, which on a position would be serious.

-Wall -Wextra say nothing, since the conversion occurs inside a library template, in a system header, where warnings are off by default. Write the start as 0.0, or use std::reduce with a typed start, or set the type yourself: accumulate(b, e, double{}).

The same trap applies with a start of 0 on a sum of long values, where each step’s long result is cut down to int, so a large total comes out silently wrong.

Classic wrong answer. “It rounds, so the answer is near 2.” Each step cuts toward zero and starts again from what remains.

Question 3.12 ★ QD, Dev, LLE • any • junior

What three lines does Listing 3.12 print? What does auto leave out when it deduces a type from a const int&? When should a range-based loop use const auto&, and when is plain auto better?

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{1, 2};
    for (auto x : v) x *= 10;
    std::cout << v[0] << " " << v[1] << "\n";
    for (auto& x : v) x *= 10;
    std::cout << v[0] << " " << v[1] << "\n";
    const int& r = v[0];
    auto a = r;
    a = 7;
    std::cout << v[0] << " " << a << "\n";
}
Listing 3.12. What auto deduces. code/interviews-3/03-cpp/cpp/range_copy.cpp
Answer

Answer to Question 3.12.

It prints 1 2, 10 20 and 10 7. The first loop changes a copy, and the second changes the elements. auto leaves out the reference and the top-level const, so a is a plain int, and writing to it leaves v alone.

To keep them, say so: auto& b = r deduces const int& and const auto& says it outright, and both bind to the element. decltype(auto) keeps the exact type of the initialiser.

For a range-based loop, use const auto& on anything that may be costly to copy, such as a string or a large struct, and auto& to change elements. Plain auto is right for small values, such as an integer or a string_view, where a copy is cheap and the code sees no aliasing.

Classic wrong answer. for (auto x : orders) x.qty = 0; “It compiled, so it worked.” It zeroed copies, with no warning, and the orders are unchanged.

Follow-up. Why can const auto& go wrong on a map? With a mismatched element type, such as const std::pair<int, double>& for a map’s pair<const int, double>, each element is converted to a temporary and bound: an unnoticed copy per step.

Question 3.13 ★★ QD, Dev, LLE • any • junior, experienced

Listing 3.13 compiles with no warning and often seems to run. What is wrong, and what would AddressSanitizer report first? State the rule of five, then write this class to follow it. Could you avoid writing any of those members?

struct Buffer {
    double* data;
    explicit Buffer(int n) : data(new double[n]{}) {}
    ~Buffer() { delete[] data; }
};

int main() {
    Buffer a(4);
    {
        Buffer b = a;
        b.data[0] = 1.0;
    }
    a.data[0] = 2.0;
}
Listing 3.13. A class that owns memory. code/interviews-3/03-cpp/cpp/buffer_bad.cpp
Answer

Answer to Question 3.13.

b = a copies the pointer, so both objects own one array. When b dies, the array is freed, and AddressSanitizer reports a heap-use-after-free at a.data[0] = 2.0. If that line were gone, a’s destructor would free the array a second time.

The compiler writes the copy operations itself unless told otherwise, and it copies members one by one, which for a raw pointer means the address, not the memory it points at.

The rule of five: if a class needs a custom destructor, copy constructor, copy assignment, move constructor or move assignment, it probably needs to think about all five. Listing 3.52 does that. Assignment by value serves both copying and moving, and its swap means self-assignment cannot go wrong. noexcept on the moves lets containers use them.

#include <algorithm>
#include <cstddef>
#include <utility>

class Buffer {
    std::size_t n_ = 0;
    double* data_ = nullptr;

public:
    explicit Buffer(std::size_t n) : n_(n), data_(new double[n]{}) {}
    ~Buffer() { delete[] data_; }
    Buffer(const Buffer& o) : n_(o.n_), data_(new double[o.n_]) {
        std::copy(o.data_, o.data_ + n_, data_);
    }
    Buffer(Buffer&& o) noexcept
        : n_(std::exchange(o.n_, 0)), data_(std::exchange(o.data_, nullptr)) {}
    Buffer& operator=(Buffer o) noexcept {       // by value: serves copy and move
        std::swap(n_, o.n_);
        std::swap(data_, o.data_);
        return *this;
    }
    double& operator[](std::size_t i) { return data_[i]; }
    std::size_t size() const { return n_; }
};
Listing 3.52. All five, with assignment by value. code/interviews-3/03-cpp/cpp/buffer_five.cpp

Better still is the rule of zero: hold a std::vector<double>, and write none of them. The compiler’s versions are then correct, since each member manages itself. Writing the five is for the few classes whose whole job is to own one resource.

Classic wrong answer. “Declare the copy constructor private and be done.” That stops the copying, and in modern C++ you would write = delete; it is fine for a type that should not be copied, but you still need to decide about moves.

Question 3.14 ★★ QD, Dev, LLE • any • junior, experienced

What two lines does Listing 3.14 print in C++20? Logged differs from Plain only by an empty destructor, added to help with logging. Which members did that remove? Is the program still correct, and what has it lost? What single line fixes it?

#include <iostream>
#include <utility>

struct Tracked {
    static inline int copies = 0, moves = 0;
    Tracked() = default;
    Tracked(const Tracked&) { ++copies; }
    Tracked(Tracked&&) noexcept { ++moves; }
};

struct Plain { Tracked t; };
struct Logged { Tracked t; ~Logged() {} };

template <class H>
void show() {
    Tracked::copies = Tracked::moves = 0;
    H a;
    H b = std::move(a);
    (void)b;
    std::cout << Tracked::copies << " " << Tracked::moves << "\n";
}

int main() {
    show<Plain>();
    show<Logged>();
}
Listing 3.14. An empty destructor. code/interviews-3/03-cpp/cpp/no_move.cpp
Answer

Answer to Question 3.14.

It prints 0 1 and then 1 0. A user-declared destructor means the compiler does not generate the move constructor or move assignment, so std::move falls back on the copy constructor. The program is still correct, and has lost only speed. The fix is the line Logged(Logged&&) = default;.

That one line is not quite all. Declaring a move constructor deletes the implicit copy operations, so Logged(const Logged&) = default; and the two assignments should follow, if they are wanted, and Logged() = default; too.

Nothing warns you, as this is a silent performance change: std::move still compiles, and a copy is a valid way to meet a request to move. So a member like a long string or a big vector is now duplicated on every move.

The lesson is about what looks harmless. A destructor added to log something looks free, and it can change how every object of the class moves. If you need to see moves, count them, as the listing does.

Classic wrong answer. “An empty destructor is the same as none.” Its body does nothing, but it is user-declared, and that is what the language looks at: the moves go, and a type that was trivially copyable stops being so, which also changes how it is passed.

Follow-up. What does = default on the destructor do? It is still user-declared, so it suppresses the moves in just the same way.

Question 3.15 ★★ QD, Dev, LLE • any • junior, experienced

With libstdc++, whose vector starts empty and doubles its capacity, what two lines does Listing 3.15 print? The two types differ only by noexcept on the move constructor. Why does the vector copy in one case? What guarantee is it protecting?

#include <iostream>
#include <vector>

template <bool NoThrow>
struct Item {
    static inline int copies = 0, moves = 0;
    Item() = default;
    Item(const Item&) { ++copies; }
    Item(Item&&) noexcept(NoThrow) { ++moves; }
};

template <class T>
void grow() {
    std::vector<T> v;
    for (int i = 0; i < 5; ++i) v.emplace_back();
    std::cout << T::copies << " copies, " << T::moves << " moves\n";
}

int main() {
    grow<Item<true>>();
    grow<Item<false>>();
}
Listing 3.15. Growth and noexcept. code/interviews-3/03-cpp/cpp/vec_noexcept.cpp
Answer

Answer to Question 3.15.

It prints 0 copies, 7 moves, then 7 copies, 0 moves. The capacity goes 1, 2, 4, then 8, so the five insertions move 1, 2 and 4 old elements. A vector moves them only if the move cannot throw. Otherwise it copies, to keep the strong exception guarantee of push_back.

The guarantee says that if the insertion throws, the vector is as it was. With copies, that is easy to keep: if the third copy throws, destroy the new copies, free the new block, and the old elements are untouched. With a throwing move, the first elements of the old block are already moved-from when the third move throws, and moving them back could throw as well, so there is no way back.

So the vector uses std::move_if_noexcept, which gives a move only if the move is noexcept or the type cannot be copied at all. A move-only type with a throwing move still moves, and the guarantee is given up.

So a move constructor that never throws should say noexcept. Without it, the moves you wrote may never run in a vector. A compiler-generated move is noexcept on its own when all the members’ moves are.

Classic wrong answer. “The compiler can see that this move cannot throw.” It checks the declaration. An inline body that does nothing still counts as potentially throwing here.

Follow-up. How do you check a type? With a static_assert next to the type, on the nothrow-move trait from <type_traits>.

Question 3.16 ★★ QD, Dev, LLE • any • junior

With libstdc++, what five lines does Listing 3.16 print? Which of those values does the standard promise, and which are only what this library does? What does std::move itself do at run time? What may you do with a moved-from std::string?

#include <iostream>
#include <memory>
#include <string>
#include <utility>

int main() {
    std::string a(40, 'x');
    std::string b = std::move(a);
    std::cout << a.size() << " " << b.size() << "\n";
    a = "new";
    std::cout << a.size() << "\n";
    int i = 1;
    int j = std::move(i);
    std::cout << i << " " << j << "\n";
    auto p = std::make_unique<int>(5);
    auto q = std::move(p);
    std::cout << (p == nullptr) << " " << *q << "\n";
    std::string&& r = std::move(a);
    r += "!";
    std::cout << a << "\n";
}
Listing 3.16. What is left after a move. code/interviews-3/03-cpp/cpp/moved_from.cpp
Answer

Answer to Question 3.16.

It prints 0 40, 3, 1 1, 1 5 and new!. The standard promises the 40, the 3, the 1 1, the 1 5 and the last line. The size 0 of a moved-from string is what libstdc++ does here, and the standard calls it only “valid but unspecified”.

std::move does nothing at run time. It is a cast to an rvalue reference, which lets a move constructor be chosen. The last two lines of the listing show that: r is just another name for a, and a changes through it, with nothing moved at all.

The rest follows from what is moved. An int has no resources, so its move is a copy, and i stays 1. A unique_ptr is promised to be null after being moved from, as its specification says so.

On a moved-from string, you may do whatever has no preconditions: assign to it, as the second line shows, destroy it, or ask for its size. You may not assume a value, so a.front() is not safe unless you check.

Classic wrong answer. “After std::move(a), a is empty.” The cast alone changes nothing, and even a real move does not promise an empty string.

Follow-up. What does a short string do? Its characters are in the object itself, so the move copies them. Nothing is cheaper there.

Question 3.17 ★★ QD, Dev, LLE • any • junior, experienced

With g++ 11.4 in C++20, what three lines does Listing 3.17 print? Why is writing std::move on the return worse than leaving it out? Why does either copy? And how can make return a type with no usable copy or move constructor?

#include <iostream>
#include <utility>

struct T {
    static inline int copies = 0, moves = 0;
    T() = default;
    T(const T&) { ++copies; }
    T(T&&) noexcept { ++moves; }
};

T plain() { T t; return t; }
T forced() { T t; return std::move(t); }
T either(bool c) { T p, q; return c ? p : q; }

struct Pinned {
    Pinned() = default;
    Pinned(const Pinned&) = delete;
};
Pinned make() { return Pinned{}; }

template <class F>
void show(const char* name, F f) {
    T::copies = T::moves = 0;
    { auto r = f(); (void)r; }
    std::cout << name << ": " << T::copies << " copies, " << T::moves << " moves\n";
}

int main() {
    show("plain", [] { return plain(); });
    show("forced", [] { return forced(); });
    show("either", [] { return either(true); });
    Pinned p = make();
    (void)p;
}
Listing 3.17. Returning a value. code/interviews-3/03-cpp/cpp/elision.cpp
Answer

Answer to Question 3.17.

It prints plain: 0 copies, 0 moves, forced: 0 copies, 1 moves and either: 1 copies, 0 moves. std::move on the return prevents elision, as the returned expression is no longer the plain local name. In either, the returned expression c ? p : q is not the name of a local, so the result is copied.

Returning a local by name lets the compiler build it directly in the caller’s slot, which g++ does here. That is allowed and not required. If it declines, the language still tries a move first, so return t; never needs std::move. So the cast gains nothing and costs a move. -Wpessimizing-move, in -Wall, warns about it, and the listing has it turned off on a line not shown.

The conditional is a different case. It is not a name, so no implicit move applies. if (c) return p; return q; would move, or could elide.

make works for a different reason. Since C++17, a function returning a prvalue directly has no temporary to copy at all: Pinned{} initialises p itself. So this is a rule of the language, and no copy constructor need exist.

Classic wrong answer. “Always write std::move on the return, to be sure it moves.” That is the one place where it is harmful. It has a use only when returning a member, or a parameter that is an rvalue reference, in older standards.

Question 3.18 ★★ QD, Dev, LLE • any • junior, experienced

What five lines does Listing 3.18 print? Why does bad always copy, even when it was handed a temporary? What does std::forward<T> do that std::move does not, and what goes wrong if you use std::move in good?

#include <iostream>
#include <string>
#include <utility>

void take(const std::string&) { std::cout << "copy\n"; }
void take(std::string&&) { std::cout << "move\n"; }

template <class T>
void bad(T&& x) { take(x); }

template <class T>
void good(T&& x) { take(std::forward<T>(x)); }

int main() {
    std::string s = "abc";
    bad(s);
    bad(std::string("abc"));
    good(s);
    good(std::string("abc"));
    good(std::move(s));
}
Listing 3.18. Handing an argument on. code/interviews-3/03-cpp/cpp/forwarding.cpp
Answer

Answer to Question 3.18.

It prints copy, copy, copy, move, move. Inside bad, x is a named variable, and any named variable is an lvalue, so take(x) always picks the const std::string& overload.

std::forward<T>(x) restores what the caller passed: an lvalue stays an lvalue, and an rvalue becomes an rvalue again. It can do this because T carries that information: for a caller’s lvalue, T is std::string&, and for an rvalue it is std::string.

std::move makes everything an rvalue, without condition. With it in good, the call good(s) would move out of the caller’s s, emptying a variable that the caller never gave up. That is a nasty bug, since the caller’s code shows no std::move.

So the rule is simple: std::forward goes with a forwarding reference, and std::move with an rvalue reference. Forward an argument once at most, as a second use reads a moved-from object.

Classic wrong answer. “x has type T&&, so it is an rvalue.” The type of the variable and the category of the expression are different things. A name is always an lvalue.

Follow-up. What goes wrong with a function pointer or a brace list? Perfect forwarding is not perfect. good({1, 2}) fails, as a brace list has no type from which to deduce T. A function pointer forwards fine, but the name of an overloaded function does not: it too has no single type from which to deduce T.

Question 3.19 ★★ QD, Dev, LLE • any • experienced

What three lines does Listing 3.19 print? Which of T&& in f, std::vector<T>&& in g, and auto&& are forwarding references, and how can you tell? What is T deduced to in f(i), and how does int& && become a legal type? Replace the comment with std::vector<int> l; g(l);: does it compile?

#include <iostream>
#include <type_traits>
#include <vector>

template <class T>
const char* f(T&&) {
    return std::is_lvalue_reference_v<T> ? "T is a reference" : "T is a value";
}

template <class T>
void g(std::vector<T>&&) {}

int main() {
    int i = 1;
    const int k = 2;
    std::cout << f(i) << "\n" << f(1) << "\n" << f(k) << "\n";
    g(std::vector<int>{});
    auto&& a = i;
    auto&& b = 1;
    static_assert(std::is_same_v<decltype(a), int&>);
    static_assert(std::is_same_v<decltype(b), int&&>);
    // try each line here
}
Listing 3.19. Two meanings of a double ampersand. code/interviews-3/03-cpp/cpp/collapse.cpp
Answer

Answer to Question 3.19.

It prints T is a reference, T is a value and T is a reference. Only T&& in f and auto&& are forwarding references, as each has the exact form of a deduced name plus &&. g(l) does not compile, since std::vector<T>&& is a plain rvalue reference and l is an lvalue.

With f(i), i is an lvalue, so a special deduction rule makes T equal to int&. The parameter type becomes int& &&. You cannot write that yourself, but it may arise from a template or an alias, and reference collapsing reduces it: if either part is &, the result is &, and only && && gives &&. So the parameter is int&. With f(1), T is int, and the parameter is int&&. With a const int, T is const int&.

Decoration of any kind means it is no longer a forwarding reference, as in const T&& or in std::vector<T>&&. A T&& in a member of a class template, where T was fixed when the class was instantiated, is not one either, because no deduction happens at that call.

So auto&& a = i makes a an int&, and auto&& b = 1 an int&&, which the listing checks with static_assert.

Classic wrong answer. “&& always means an rvalue reference.” Only where nothing is deduced.

Question 3.20 ★★ QD, Dev, LLE • any • junior, experienced

Listing 3.20 compiles with no warning under g++ 11.4, -Wall -Wextra. What is wrong? What does AddressSanitizer report? Would the bug show with a string of 5 characters? How do you take a string_view safely, and where is it a good parameter type but a poor member?

#include <iostream>
#include <string>
#include <string_view>

std::string make() { return std::string(40, 'x'); }

int main() {
    std::string_view v = make();
    std::cout << v.size() << " " << v[0] << "\n";
}
Listing 3.20. A view of a temporary. code/interviews-3/03-cpp/cpp/sv_temp.cpp
Answer

Answer to Question 3.20.

make() returns a temporary, which is destroyed at the end of the declaration, so v points at freed memory. AddressSanitizer reports heap-use-after-free at the line that reads v. A 5-character string would be held inside the std::string object, by the small-string optimisation, so no heap is involved, and the view would dangle into dead stack instead.

A string_view is a pointer and a length with no ownership. Binding a const std::string& to a temporary extends the temporary’s life. A view has no such effect, as its conversion runs through an operator, and the temporary dies as usual. So keep the string in a named variable, and take the view from that.

As a parameter, a view is excellent: void parse(std::string_view) accepts literals and strings with no copy, and the caller’s argument outlives the call. As a member, or a return value, it is poor, since the object can outlive what it views, and nothing in the type says so. If a class needs to keep the text, it should hold a std::string.

Classic wrong answer. “It prints the right thing, so it is fine.” Freed memory often still holds the old bytes. The program is undefined, and it passes until an allocation reuses the block.

Follow-up. Is the same true of a std::span? Yes. It is a pointer and a size, and has the same lifetime rule.

Question 3.21 ★★ QD, Dev, LLE • any • junior

What two lines does Listing 3.21 print? A string_view is passed to a C function through .data(). Why is that a bug, and why does it often pass its tests? Is it undefined here?

#include <cstring>
#include <iostream>
#include <string>
#include <string_view>

int main() {
    std::string_view line = "ESZ6,4501.25";
    std::string_view sym = line.substr(0, 4);
    std::cout << sym.size() << " " << std::strlen(sym.data()) << "\n";
    std::cout << std::strlen(std::string(sym).c_str()) << "\n";
}
Listing 3.21. A view and the end of a string. code/interviews-3/03-cpp/cpp/sv_cstr.cpp
Answer

Answer to Question 3.21.

It prints 4 12, then 4. strlen reads until a zero byte. sym.data() points into the whole literal, and the only zero byte is at its end, so the C function sees ESZ6,4501.25, 12 characters, while the view has only 4.

So the bug is an assumption: a view is a pointer plus a length, and no zero byte need follow it. A C function has no way to receive the length, so it reads on. The substring of a longer text ends in the middle of other characters, and that is the usual case for a parser.

It is not undefined here, as the literal really does have a zero byte at the end. It is a wrong answer. With a view into a non-terminated buffer, such as bytes off a socket, strlen would run off the end, which is undefined.

It passes tests because a view of an entire literal does end at a zero byte, so the simple case works. Only a true substring shows it. The fix for a C call is to make a terminated copy, std::string(sym).c_str(), as in the second line, and to keep that string alive through the call. Or use a function that takes a length.

Classic wrong answer. “string_view has c_str(), surely.” It has no such member, exactly because it cannot promise termination.

Question 3.22 ★★ QD, Dev, LLE • any • junior

What does Listing 3.22 print? Which line never prints, and why? What does a leak checker say? Fix it with std::weak_ptr, and say how you use the other node once its pointer is weak.

#include <iostream>
#include <memory>

struct Node {
    std::shared_ptr<Node> next;
    ~Node() { std::cout << "freed\n"; }
};

int main() {
    auto a = std::make_shared<Node>();
    auto b = std::make_shared<Node>();
    a->next = b;
    b->next = a;
    std::cout << a.use_count() << " " << b.use_count() << "\n";
}
Listing 3.22. Two nodes that hold each other. code/interviews-3/03-cpp/cpp/cycle.cpp
Answer

Answer to Question 3.22.

It prints 2 2, and freed never prints. Each node is held by a local and by the other node. When the locals go, each count falls to 1, so neither reaches 0. LeakSanitizer reports 64 bytes in 2 allocations. Listing 3.53 breaks the cycle with one weak link, and the test shows both destructors running.

#include <iostream>
#include <memory>

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;
    ~Node() { std::cout << "freed\n"; }
};

int main() {
    std::weak_ptr<Node> watch;
    {
        auto a = std::make_shared<Node>();
        auto b = std::make_shared<Node>();
        a->next = b;
        b->prev = a;
        watch = a;
        std::cout << a.use_count() << " " << b.use_count() << "\n";
        if (auto p = b->prev.lock()) std::cout << "alive " << p.use_count() << "\n";
    }
    std::cout << watch.expired() << " " << (watch.lock() == nullptr) << "\n";
}
Listing 3.53. One strong link and one weak one. code/interviews-3/03-cpp/cpp/cycle_fixed.cpp

Reference counting cannot see a cycle, as it only knows local counts, and nothing outside now refers to the pair. A garbage collector that traces from roots would find them, but C++ has none.

The choice is about which link means ownership. Here next owns and prev watches. A weak_ptr does not keep the object alive and cannot be dereferenced directly, so you must call lock(). That gives a shared_ptr, or null if the object has gone, and that is why the listing tests it. While you hold the result, the object cannot die under you. That is the count of 2 on the line alive 2.

After the block, expired() is true, and lock() gives null.

Classic wrong answer. “use_count tells me whether it is safe to use.” On several threads it can be stale the moment it returns. lock() is the atomic check.

Follow-up. What does a raw pointer offer over a weak_ptr here? It costs nothing, and is fine if the owner surely outlives it. A weak_ptr is for when it may not.

Question 3.23 ★★ QD, Dev, LLE • any • junior

Write a simplified std::unique_ptr<T> in C++20, with no custom deleter: a pointer constructor, destructor, move construction and assignment, no copying, and get, release, reset and the operators. What happens on self-move assignment? Why does reset store the new pointer before deleting the old one? How large is your class?

Answer

Answer to Question 3.23.

Listing 3.54 is one such class, as large as a raw pointer, 8 bytes on x86-64. Self-move is safe, as release empties the object first and reset takes the same pointer back. It behaves like std::unique_ptr on 20 000 random operations, with no leak under AddressSanitizer.

template <class T>
class Unique {
    T* p_ = nullptr;

public:
    Unique() = default;
    explicit Unique(T* p) : p_(p) {}
    ~Unique() { delete p_; }
    Unique(const Unique&) = delete;
    Unique& operator=(const Unique&) = delete;
    Unique(Unique&& o) noexcept : p_(std::exchange(o.p_, nullptr)) {}
    Unique& operator=(Unique&& o) noexcept {
        reset(o.release());                  // safe on self-move
        return *this;
    }
    T* release() noexcept { return std::exchange(p_, nullptr); }
    void reset(T* p = nullptr) noexcept {
        T* old = std::exchange(p_, p);       // the object may hold a Unique to us
        delete old;
    }
    T* get() const noexcept { return p_; }
    T& operator*() const { return *p_; }
    T* operator->() const noexcept { return p_; }
    explicit operator bool() const noexcept { return p_ != nullptr; }
};
Listing 3.54. A small owning pointer. code/interviews-3/03-cpp/cpp/unique.hpp

Ownership means one owner, so copying is deleted, and a move hands over the pointer and leaves null. Both are noexcept, so a vector of these moves during growth.

reset stores the new pointer first because the destructor of the old object may reach back here. If a node owns a child through a Unique, and that child’s destructor ends up calling reset on the same pointer, an old-first version would delete the object twice. std::exchange leaves the member consistent before any outside code runs.

explicit on the constructor matters. Without it, a raw pointer would convert silently, and passing one to a function taking a Unique would delete it at the end of the call.

delete nullptr is defined and does nothing, so no check is needed in the destructor. The type must be complete at that point, or the compiler cannot call its destructor, which matters when the class is used with a forward declaration, as in the pimpl idiom.

Classic wrong answer. delete p_; p_ = o.p_; o.p_ = nullptr; in move assignment, with no check. On self-move it deletes the object and ends up null: the value is silently destroyed.

Follow-up. What does the real one add? A deleter type, array support, and conversion from a pointer to a derived class.

Question 3.24 ★★ QD, Dev, LLE • any • junior, experienced

A type holds a megabyte, and Listing 3.23 replaces operator delete to count frees. With libstdc++, what does it print? When does the megabyte go back in each case, and why do the two differ? Give a case for avoiding make_shared, and the case that usually favours it.

static long frees = 0;
[[gnu::noinline]] void* operator new(std::size_t n) {
    if (void* p = std::malloc(n)) return p;
    throw std::bad_alloc{};
}
[[gnu::noinline]] void operator delete(void* p) noexcept { ++frees; std::free(p); }
[[gnu::noinline]] void operator delete(void* p, std::size_t) noexcept {
    ++frees;
    std::free(p);
}

struct Big {
    char bytes[1'000'000];
    ~Big() { std::puts("destroyed"); }
};

template <class Make>
void run(const char* name, Make make) {
    frees = 0;
    std::weak_ptr<Big> w;
    {
        std::shared_ptr<Big> s = make();
        w = s;
    }
    std::printf("%s: %ld frees while the weak_ptr lives\n", name, frees);
    w.reset();
    std::printf("%s: %ld frees after\n", name, frees);
}

int main() {
    run("new", [] { return std::shared_ptr<Big>(new Big); });
    run("make_shared", [] { return std::make_shared<Big>(); });
}
Listing 3.23. When the memory goes back. code/interviews-3/03-cpp/cpp/make_shared_weak.cpp
Answer

Answer to Question 3.24.

Both print destroyed when the last shared_ptr goes. With new, there is 1 free then and 2 after the weak_ptr goes. With make_shared, there are 0 frees then, and 1 after. So in the second case, the megabyte stays allocated until the last weak_ptr has gone.

make_shared puts object and control block in one allocation. The control block must outlive the object, since a weak_ptr needs its counts to answer expired(). The destructor runs when the strong count reaches 0, but the memory is one block, so it cannot be returned in part. With new, there are two blocks, and the object’s goes first.

So avoid make_shared for a large object watched by long-lived weak pointers, such as a cache holding weak references. A destructor that frees a member’s memory does help, as only the object’s own bytes stay.

Otherwise, make_shared is the usual choice. One allocation instead of two means less time and less overhead, and object and counts are close together, which helps the cache. You also do not write new. It cannot take a custom deleter, and it needs a public constructor.

Classic wrong answer. “The destructor has run, so the memory is free.” Destruction and deallocation are two steps, and here they happen at different times.

Question 3.25 ★★ QD, Dev, LLE • any • junior

What is wrong with Listing 3.24, and what do the two counts show? A method of a class wants to hand out a shared_ptr to its own object. Why is std::shared_ptr<Order>(this) the same bug? What is the right way, and what happens if the object is not already owned by a shared_ptr?

#include <iostream>
#include <memory>

struct Order {
    int id = 7;
    ~Order() { std::cout << "gone\n"; }
};

int main() {
    Order* raw = new Order;
    std::shared_ptr<Order> a(raw);
    std::shared_ptr<Order> b(raw);
    std::cout << a.use_count() << " " << b.use_count() << "\n";
}
Listing 3.24. Two owners of one raw pointer. code/interviews-3/03-cpp/cpp/two_owners.cpp
Answer

Answer to Question 3.25.

a and b each create their own control block for one object, and each count is 1, so they do not know about each other. Whichever dies second deletes the object again, and AddressSanitizer reports attempting double-free.

Ownership is shared only by copying a shared_ptr. Constructing from a raw pointer always starts a new count. So keep the raw pointer out of it: auto a = std::make_shared<Order>(); auto b = a; gives counts of 2.

std::shared_ptr<Order>(this) starts a second count on an object already owned, which is the same bug, with the two owners even further apart.

The right way, in Listing 3.55, is to derive from std::enable_shared_from_this and call shared_from_this(). When a shared_ptr takes ownership, it stores a weak_ptr to itself inside that base, and the call locks it, which shares the same control block.

#include <iostream>
#include <memory>

struct Order : std::enable_shared_from_this<Order> {
    std::shared_ptr<Order> self() { return shared_from_this(); }
};

int main() {
    auto a = std::make_shared<Order>();
    auto b = a->self();
    std::cout << a.use_count() << " " << b.use_count() << "\n";
    Order plain;
    try {
        plain.self();
    } catch (const std::bad_weak_ptr&) {
        std::cout << "bad_weak_ptr\n";
    }
}
Listing 3.55. Handing out a pointer to yourself. code/interviews-3/03-cpp/cpp/share_this.cpp

So the object must already be owned by a shared_ptr. If it is not, as with plain, the weak pointer is empty, and since C++17 the call throws std::bad_weak_ptr. It is also unavailable in the constructor, since no shared_ptr has yet taken ownership.

Classic wrong answer. “The counts are atomic, so two owners are safe.” Atomicity is about one count seen by many threads. There are two counts here.

Question 3.26 ★★ QD, Dev, LLE • any • junior

Listing 3.25 may print 9 or 1, or something else. Why? What does AddressSanitizer report, and on which line? Give two fixes, one of which keeps the reference. Does the same code on a std::deque have this bug?

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{1, 2, 3};
    int& first = v[0];
    v.push_back(4);
    first = 9;
    std::cout << v[0] << "\n";
}
Listing 3.25. A reference into a vector. code/interviews-3/03-cpp/cpp/vec_ref.cpp
Answer

Answer to Question 3.26.

push_back needs more capacity than the vector’s 3, so it moves the elements to a new block and frees the old one. first still points into the freed block. AddressSanitizer reports heap-use-after-free at the line first = 9.

Any operation that reallocates invalidates all references, pointers and iterators into a vector. If the capacity is not exceeded, those before the insertion point stay valid. So this bug depends on the capacity at the time, which is why it hides: a vector that already had spare room runs correctly.

Two fixes. Call v.reserve(4) first, and then the reference stays valid, as the test confirms with a clean run under the sanitizer. That is fragile, since the reserve has to cover every later insertion. Or hold an index, std::size_t first = 0, and write v[first] = 9, which survives any reallocation, and is the usual answer.

std::deque has different rules. push_back and push_front invalidate iterators but not references to elements, so this exact code is correct on a deque. An insertion in the middle invalidates both.

Classic wrong answer. “It printed 1 here, so the write was lost, and that is the only harm.” Writing to freed memory can corrupt another object or the allocator’s own data, far from this line.

Follow-up. What about a range-for that pushes onto the vector it walks? Same bug: its iterators are invalidated by the first reallocation.

Question 3.27 ★★ QD, Dev, LLE • any • junior, experienced

With libstdc++, what does Listing 3.26 print? Why do auto on a vector<int> and on a vector<bool> behave differently? Does bool& b = v[0]; compile? Two threads each write a different index of a vector<bool>: is that a data race, and is it one for vector<char>?

#include <iostream>
#include <vector>

int main() {
    std::vector<int> w{0, 0};
    auto y = w[0];
    y = 1;
    std::vector<bool> v{false, false};
    auto x = v[0];
    x = true;
    std::cout << w[0] << " " << y << " " << v[0] << "\n";
    // try each line here
}
Listing 3.26. A vector unlike the others. code/interviews-3/03-cpp/cpp/vec_bool.cpp
Answer

Answer to Question 3.27.

It prints 0 1 1. auto y = w[0] copies an int. vector<bool> stores bits, and a bit has no address, so v[0] returns a small proxy object, and auto x is that proxy. Assigning to x writes through to the bit, so v[0] becomes true.

bool& b = v[0]; does not compile, since the proxy is not a bool. Neither does auto& b = v[0];, because the proxy is a temporary. bool b = v[0]; compiles, and it makes an independent copy, which is what auto did not do.

Two different indices of vector<bool> can share a byte, so writing them means reading and writing that byte, and the standard does not make this safe: vector<bool> is the named exception to the rule that different elements of a container can be written concurrently. ThreadSanitizer reports a data race in the test for vector<bool>, and none for vector<char>.

So vector<bool> is not really a container of bool: bool* cannot point at an element, and generic code that takes a T& fails. Use vector<char> when you want the usual behaviour, and std::bitset or vector<bool> when you want the memory saving of one bit each.

Classic wrong answer. “It saves memory, so it is always better.” Each access takes a shift and a mask, and the race rule is different. On a tight loop, bytes can be faster.

Question 3.28 ★★ QD, Dev, LLE • any • junior, experienced

With g++ 11.4 and libstdc++, what three lines does Listing 3.27 print? Which results does the standard promise? What does a rehash invalidate in a std::unordered_map? You keep pointers to orders held inside a container: which containers allow it?

#include <iostream>
#include <map>
#include <unordered_map>
#include <vector>

int main() {
    std::unordered_map<int, int> u;
    u[1] = 1;
    int* pu = &u[1];
    auto buckets = u.bucket_count();
    for (int i = 2; i < 1000; ++i) u[i] = i;
    std::cout << (u.bucket_count() != buckets) << " " << (pu == &u[1]) << "\n";

    std::map<int, int> m;
    m[1] = 1;
    int* pm = &m[1];
    for (int i = 2; i < 1000; ++i) m[i] = i;
    std::cout << (pm == &m[1]) << "\n";

    std::vector<int> v(1);
    int* pv = &v[0];
    for (int i = 2; i < 1000; ++i) v.push_back(i);
    std::cout << (pv == &v[0]) << "\n";
}
Listing 3.27. Does an element stay put? code/interviews-3/03-cpp/cpp/stability.cpp
Answer

Answer to Question 3.28.

It prints 1 1, 1 and 0. The standard promises the second number of the first line and the second line: references and pointers into unordered_map and map stay valid across insertions. The first number says that the table rehashed, which depends on the starting bucket count that the library chooses. The 0 says that the vector moved, and no promise covers that value either.

A rehash invalidates iterators of an unordered_map, and not references or pointers to its elements. The elements live in separately allocated nodes in libstdc++, and the standard’s rule forces such a design. That is why unordered_map cannot be a flat open-addressed table, which is the price of this promise.

For pointers to orders held in a container: std::map, std::unordered_map, std::list and std::deque (at the ends) all allow it. std::vector does not, unless the capacity is fixed in advance. So a book often keeps orders in a pool with stable addresses, with an index on top, or stores integer indices, which also make better use of the cache.

Erasing is a separate matter. It invalidates references only to the erased element, on map and unordered_map.

Classic wrong answer. “An unordered_map moves everything on rehash, so pointers die.” That confuses it with a vector. Its nodes stay put, and only the bucket array is rebuilt.

Follow-up. What does a flat hash table give up to run faster? Exactly this: its elements move on growth, so pointers do not survive.

Question 3.29 ★★ QD, Dev, LLE • any • junior, experienced

What three lines does Listing 3.28 print? When is emplace_back actually better than push_back? Replace the comment with rows.push_back(1000000);: what happens, and why is that better than what emplace_back did on the line above?

#include <iostream>
#include <vector>

struct T {
    static inline int moves = 0;
    int x;
    T(int v) : x(v) {}
    T(T&& o) noexcept : x(o.x) { ++moves; }
};

int main() {
    std::vector<T> a;
    a.reserve(4);
    a.push_back(T(1));
    std::cout << T::moves << "\n";
    a.emplace_back(2);
    std::cout << T::moves << "\n";
    std::vector<std::vector<int>> rows;
    rows.emplace_back(1'000'000);
    std::cout << rows[0].size() << "\n";
    // try each line here
}
Listing 3.28. Two ways to add an element. code/interviews-3/03-cpp/cpp/emplace.cpp
Answer

Answer to Question 3.29.

It prints 1, 1 and 1000000. The first count comes from push_back(T(1)), which builds a temporary and moves it in. emplace_back(2) builds in place, with no move, so the count stays at 1. The push_back of a plain 1000000 does not compile, and that is the point.

emplace_back helps when you pass constructor arguments and a move is costly, or not available at all. If you already have an object, push_back(std::move(x)) and emplace_back(std::move(x)) do the same work. For a cheap type such as a small struct of integers, the difference is near nothing once optimised.

The danger is that emplace_back can call explicit constructors. The vector constructor taking a size is explicit so that a number cannot quietly become a vector. So the emplace_back in the listing makes a row of a million zeros, where the author may have meant a row holding one number. push_back needs an implicit conversion, and there is none, so the mistake is caught. With braces, push_back({1000000}) gives the row of one element.

So use push_back by default, for its checking, and emplace_back when you are really building in place.

Classic wrong answer. “emplace_back is always faster, so use it everywhere.” It can be equal, it can be slower to compile, and it switches off a safety check.

Question 3.30 ★★ QD, Dev, LLE, QR • any • junior, experienced

Listing 3.29 sorts 100 equal values with a comparator written as a <= b. With g++ 11.4, what happens under AddressSanitizer? What rule did the comparator break? Why might the same code pass on short or distinct inputs? How would you write a comparator on two fields?

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v(100, 1);
    std::sort(v.begin(), v.end(), [](int a, int b) { return a <= b; });
    std::cout << v.size() << "\n";
}
Listing 3.29. A comparator with an equals sign. code/interviews-3/03-cpp/cpp/sort_le.cpp
Answer

Answer to Question 3.30.

AddressSanitizer reports a heap-buffer-overflow, a read past the end, in __unguarded_partition, inside the sort. The comparator broke the requirement of a strict weak ordering: comp(a, a) must be false, and with <= it is true. Breaking it makes the call undefined, so this crash is one allowed result among many.

The implementation depends on the promise. libstdc++’s partition step scans forward without a bounds check, on the guarantee that median-of-three left an element in the range that is not less than the pivot. With <=, an equal element also counts as less, so the scan does not stop there and walks off the end of the array.

It passes on small inputs, as libstdc++ uses insertion sort for short ranges, with no such step: the threshold is 16 elements in this version. It passes on distinct values too, because <= and < then give the same answers. So only many equal keys show it, which is what happens the first time a real data set arrives with repeated prices.

For two fields, compare a tuple: std::tie(a.px, a.t) < std::tie(b.px, b.t), which gets the logic right, or in C++20 a defaulted operator<=>. Writing it by hand invites the mistake: a.px < b.px || a.t < b.t is not a strict weak ordering either.

Classic wrong answer. “It sorts a little oddly, but the worst case is a wrong order.” A broken comparator can read and write outside the array.

Follow-up. What other things need this? std::map, std::set and heaps rely on the same rule.

Question 3.31 ★★ QD, Dev, LLE • any • junior, experienced

With g++ 11.4, -std=c++20 -O2, x86-64, what does Listing 3.30 print? The first function is a common attempt to detect overflow, for b>0b > 0. Why is it wrong, and what does the compiler do with it? What do UBSan and -fwrapv change? Write a portable check that does not use a built-in.

#include <climits>
#include <iostream>

[[gnu::noinline]] bool overflows(int a, int b) {     // for b > 0
    return a + b < a;
}

[[gnu::noinline]] bool safely(int a, int b) {
    int r;
    return __builtin_add_overflow(a, b, &r);
}

int main() {
    std::cout << overflows(INT_MAX, 1) << " " << safely(INT_MAX, 1) << "\n";
}
Listing 3.30. Checking for overflow too late. code/interviews-3/03-cpp/cpp/overflow_check.cpp
Answer

Answer to Question 3.31.

It prints 0 1: the first check misses the overflow, and the built-in catches it. Signed overflow is undefined, so the compiler may assume that a + b does not overflow. For that case, a + b < a is the same as b < 0, which is what it compiles.

With b=1b = 1, that test is false, whatever aa is, so the check can never fire. The code reads as if overflow wraps, and on the hardware it does, but the optimiser works from the language’s rules, not from the machine’s.

Under UBSan, the run reports signed integer overflow: 2147483647 + 1 and prints 1 1, since the sanitizer’s build does the addition and it wraps. With -fwrapv, overflow is defined to wrap, so the check works, and the program prints 1 1 here too. Both change what is being asked, and neither makes the code correct C++.

The portable check tests before adding: for b>0b > 0, a > INT_MAX - b, and for b<0b < 0, a < INT_MIN - b. Neither side can overflow. __builtin_add_overflow is a compiler extension in g++ and Clang that does the same, and here it reads the CPU’s overflow flag with a seto, as our test confirms.

Classic wrong answer. “It must be a compiler bug, since the hardware wraps.” The program has the bug. g++ 11.4 applies this rewrite even at -O0: the listing prints 0 1 there too.

Question 3.32 ★★ QD, Dev, LLE • any • junior, experienced

With g++ 11.4, C++20, -O2, x86-64, run with no arguments, what four lines does Listing 3.31 print? For each line, is the value promised by the standard, or undefined? What does the shift print and why? Which of them would UBSan flag?

#include <climits>
#include <cstdlib>
#include <iostream>

int main(int argc, char**) {
    int s = argc + 32;                       // 33 with no arguments
    int low = INT_MIN + argc - 1;            // INT_MIN with no arguments
    std::cout << (1 << 31) << "\n";
    std::cout << (1 << s) << "\n";
    std::cout << std::abs(low) << "\n";
    unsigned u = 0;
    std::cout << u - 1 << "\n";
}
Listing 3.31. Four integer edge cases. code/interviews-3/03-cpp/cpp/shift_abs.cpp
Answer

Answer to Question 3.32.

It prints -2147483648, 2, -2147483648 and 4294967295. The first and last are defined by the standard, and the second and third are undefined, so UBSan flags those two.

1 << 31 is defined since C++20, as signed integers are two’s complement and the bits go where they go, which makes this INT_MIN. Before C++20, its value was implementation-defined. u - 1 on an unsigned 0 wraps to 232−12^{32} - 1, which is always defined.

1 << 33 on a 32-bit type has a shift count at or above the width, which remains undefined in C++20. It prints 2 because x86’s shift instruction uses only the low 5 bits of the count, so 33 acts as 1. That is this machine, and nothing obliges another machine or another compiler to agree.

std::abs(INT_MIN) has no answer in int, as 2312^{31} does not fit, so it is undefined. On this machine the negation wraps back, which leaves a negative absolute value. Any code that assumes an absolute value is non-negative then fails, for instance by using it as an index modulo a table size.

UBSan’s reports read shift exponent 33 is too large and negation of -2147483648.

Classic wrong answer. “Undefined means crash.” Here both undefined lines print plausible numbers. That is the usual way, which is why you need the sanitizer.

Follow-up. And INT_MIN / -1? Also undefined, as the true answer does not fit, and on x86-64 the division instruction traps. Our test sees SIGFPE.

Question 3.33 ★★ QD, Dev, LLE • any • junior, experienced

In Listing 3.32, which of the references one, two and three are safe to read after their declaration? State the rule. Why does passing the temporary through a function that returns its own argument defeat it? What would AddressSanitizer report for the last one?

#include <iostream>
#include <string>

struct Holder { std::string name; };

std::string make() { return std::string(40, 'x'); }
const std::string& same(const std::string& s) { return s; }

int main() {
    const std::string& one = make();
    const std::string& two = Holder{make()}.name;
    std::cout << one.size() << " " << two.size() << "\n";
    const std::string& three = same(make());
    std::cout << three.size() << "\n";
}
Listing 3.32. Binding a temporary to a reference. code/interviews-3/03-cpp/cpp/lifetime.cpp
Answer

Answer to Question 3.33.

one and two are safe, and three dangles. Binding a temporary directly to a const reference extends the temporary’s life to that of the reference, and this also covers a member of the temporary, which is two. AddressSanitizer reports stack-use-after-scope at the last read.

The extension is a property of the initialisation, and the compiler applies it where it sees the temporary being bound. It does not look inside a function. For same(make()), the temporary binds to the parameter s. That lasts until the end of the full expression, and the function returns a reference to it. The compiler cannot know, in general, that the returned reference refers to that argument, so three is bound to an object that is about to die.

The same goes for anything that returns a reference to its argument, which includes std::max, std::min and std::move. A member function that returns a reference to a member of a temporary is the same: Holder{make()}.get_name() would dangle, where the direct .name did not.

So when in doubt, take a value. std::string three = same(make()); copies while the temporary is still alive, and costs one copy.

Classic wrong answer. “A const reference always keeps a temporary alive.” Only one bound to it directly, in the declaration.

Follow-up. Is the range-for form safe? for (auto& x : make_vector()) is, as the range is bound directly. A chain such as make_holder().items() is not, before C++23.

Question 3.34 ★★ QD, Dev, LLE • any • junior, experienced

What four lines does Listing 3.33 print? Why does the first say Derived 1? Why does d.g(1) convert 1 to a double when Base has a better match? Give the fix for each, and say which one a compiler option can warn about.

#include <iostream>

struct Base {
    virtual void f(int x = 1) { std::cout << "Base " << x << "\n"; }
    void g(int) { std::cout << "Base::g(int)\n"; }
    virtual ~Base() = default;
};

struct Derived : Base {
    void f(int x = 2) override { std::cout << "Derived " << x << "\n"; }
    void g(double) { std::cout << "Derived::g(double)\n"; }
};

int main() {
    Derived d;
    Base& b = d;
    b.f();
    d.f();
    b.g(1);
    d.g(1);
}
Listing 3.33. Defaults and hiding. code/interviews-3/03-cpp/cpp/default_args.cpp
Answer

Answer to Question 3.34.

It prints Derived 1, Derived 2, Base::g(int) and Derived::g(double). Default arguments are chosen at compile time from the static type, and the function body at run time from the object. Names in a derived class hide all base-class overloads of that name.

So b.f() takes its default from Base, as b is declared a Base&, and it then dispatches to Derived::f, which gets 1. Two defaults for one function are a trap with no warning. The fix is not to redefine defaults on an override. Better, put the default in one non-virtual function that calls a virtual function with no defaults.

Lookup for d.g(1) stops at the first scope that holds the name, which is Derived. Base::g(int) is not even considered, so 1 converts to double. The fix is using Base::g; in Derived, which brings the base overloads into the set, and then d.g(1) picks the exact match.

A compiler option can warn about one case: -Woverloaded-virtual reports a function hiding a virtual function of the base. Here g is not virtual, so that option does not help, and the listing compiles clean. So review for it.

Classic wrong answer. “override would have caught this.” It checks that f overrides something. It says nothing of defaults, and g is not virtual.

Question 3.35 ★★ QD, Dev, LLE • any • junior, experienced

With g++ 11.4, libstdc++ and x86-64, what three lines does Listing 3.34 print? Why is an empty struct not of size 0, and why is Base1 smaller than Member? Why does a lambda deleter cost nothing, and a function pointer deleter cost 8 bytes? What does the 16 of shared_ptr hold?

#include <functional>
#include <iostream>
#include <memory>
#include <string>
#include <vector>

struct Empty {};
struct Virt { virtual ~Virt() = default; };
struct Base1 : Empty { int x; };
struct Member { Empty e; int x; };
struct Attr { [[no_unique_address]] Empty e; int x; };
void close_it(int* p) { delete p; }

int main() {
    auto lambda = [](int* p) { delete p; };
    std::cout << sizeof(Empty) << " " << sizeof(Virt) << " " << sizeof(Base1) << " "
              << sizeof(Member) << " " << sizeof(Attr) << "\n";
    std::cout << sizeof(std::unique_ptr<int>) << " "
              << sizeof(std::unique_ptr<int, decltype(lambda)>) << " "
              << sizeof(std::unique_ptr<int, void (*)(int*)>) << "\n";
    std::cout << sizeof(std::shared_ptr<int>) << " " << sizeof(std::function<void()>) << " "
              << sizeof(std::string) << " " << sizeof(std::vector<int>) << "\n";
}
Listing 3.34. What things occupy. code/interviews-3/03-cpp/cpp/sizes.cpp
Answer

Answer to Question 3.35.

It prints 1 8 4 8 4, 8 8 16 and 16 32 32 24. An object must have a unique address, so two elements of an array of Empty need different addresses, and the size must be at least 1. Base1 is 4 and Member is 8 by the empty base optimisation.

An empty base can share an address with the first member, as a base subobject need not be distinct in that way. An empty member, however, takes a byte, and padding rounds Member up to 8. C++20’s [[no_unique_address]] gives the member the same treatment, which is why Attr is 4. Virt is 8 for its pointer to a virtual table.

A lambda with no captures has an empty type, and its call is known from the type. libstdc++’s unique_ptr stores the deleter so that it occupies nothing, so the size stays 8. A function pointer deleter is a real value, chosen at run time, and must be stored, which gives 16.

shared_ptr holds two pointers: one to the object, and one to the control block with the counts. Two are needed because the object pointer may differ from the owned object, for a base class or with aliasing.

std::function is 32 here, since it holds a small buffer for the callable. std::string is 32, with room for 15 characters, and a vector holds three pointers. The standard fixes none of these sizes.

Classic wrong answer. “An empty struct takes no space.” As a base, usually so. As a member or an array element it does, unless told otherwise.

Question 3.36 ★★ QD, Dev, LLE • any • junior

A header declares template <class T> T total(const std::vector<T>& v);. Its body is in sum.cpp, and main.cpp includes the header and calls total on a std::vector<int>. Each file compiles, and then the build fails. At which stage, and why? Give the two fixes. What does the second one cost a user who wants std::vector<long>?

Answer

Answer to Question 3.36.

It fails at link time, with undefined reference to total<int>. Each file is compiled alone. main.cpp sees only the declaration, so it assumes that someone else provides total<int>. sum.cpp sees the body, but nothing there asks for any T, so it generates no code.

A template is a recipe, and it produces code only when instantiated with a type, and only where the compiler can see both the request and the recipe. No single file here has both.

Two fixes. The usual one is to put the body in the header, so that every caller can instantiate it. A template definition may appear in several files, and the linker keeps one copy.

The second is explicit instantiation. Leave the body in sum.cpp and add the line template int total(const std::vector<int>&); there, which makes it generate that one version. This keeps headers light and speeds up builds, and our test builds and links with it.

The cost is the closed list. A caller with std::vector<long> gets the same link error, as the test shows, until someone adds a second line to sum.cpp. A user of a compiled library cannot add one at all.

Classic wrong answer. “export solves this.” That keyword’s template meaning was removed in C++11. C++20 modules are the modern route, and they need build-system support.

Follow-up. What does the header form cost? Build time: each file that includes it parses the body and may instantiate it, and extern template can reduce that.

Question 3.37 ★★ QD, Dev, LLE • any • junior, experienced

What two lines does Listing 3.35 print? How does the compiler choose between the two versions of kind? Replace the comment with notional(1, 2);: what happens, and where does the error point? How would you have written this constraint before C++20?

#include <concepts>
#include <iostream>
#include <type_traits>

template <std::integral T>
const char* kind(T) { return "integer"; }

template <std::floating_point T>
const char* kind(T) { return "real"; }

template <class T>
concept Priced = requires(const T& t) {
    { t.price() } -> std::convertible_to<double>;
};

template <Priced T>
double notional(const T& t, int qty) { return t.price() * qty; }

struct Fill { double price() const { return 2.5; } };

int main() {
    std::cout << kind(1) << " " << kind(1.0) << " " << kind(1L) << "\n";
    std::cout << notional(Fill{}, 4) << "\n";
    // try each line here
}
Listing 3.35. Constraining a template. code/interviews-3/03-cpp/cpp/concepts.cpp
Answer

Answer to Question 3.37.

It prints integer real integer, then 10. Each kind is viable only if its concept holds for the argument type, and so exactly one survives. notional(1, 2) fails with an error at the call, saying that the constraint Priced<int> is not satisfied, and which requirement failed.

A concept is a named compile-time predicate over types. requires(const T& t) { …} tests whether the expressions in it would compile, and it can check the result type too. So Priced asks that t.price() be valid and convert to double. Here, int has no member price. If two constrained overloads both apply, the more constrained one is chosen.

Without a constraint, the template would be accepted for int, and the error would come from inside its body, on the line t.price() * qty. In a deep library, that message can run to hundreds of lines, and points nowhere near the mistake.

Before C++20, the usual means was SFINAE: an extra template parameter defaulted to std::enable_if_t<std::is_integral_v<T>> removes the overload silently when deduction fails. It works and reads poorly, and two overloads differing only by such a default argument are a redefinition, so you need a dummy parameter to separate them. if constexpr covers some cases more simply, from C++17.

Classic wrong answer. “A concept is a kind of base class.” It has no objects and adds no virtual calls, and a type needs no declaration to satisfy one.

Question 3.38 ★★ QD, Dev, LLE • crypto, market maker, hedge fund • junior, experienced

Market data messages are one of three types, and Listing 3.36 holds them in a std::variant. With g++ 11.4 on x86-64, what four lines does it print? How does this compare with a base class and three derived classes, for allocation and for adding a new type? If a new alternative’s constructor throws during emplace, can a variant end up holding nothing?

#include <iostream>
#include <variant>

struct Add { int id; int qty; };
struct Cancel { int id; };
struct Trade { int qty; long px; };
using Msg = std::variant<Add, Cancel, Trade>;

template <class... F>
struct Overload : F... { using F::operator()...; };

long apply(const Msg& m) {
    return std::visit(Overload{
        [](const Add& a) -> long { return a.qty; },
        [](const Cancel&) -> long { return 0; },
        [](const Trade& t) -> long { return t.qty * t.px; },
    }, m);
}

int main() {
    Msg m = Trade{3, 100};
    std::cout << apply(m) << " " << m.index() << " " << sizeof(Msg) << "\n";
    std::cout << (std::get_if<Add>(&m) == nullptr) << "\n";
    try {
        std::get<Add>(m);
    } catch (const std::bad_variant_access&) {
        std::cout << "bad access\n";
    }
    m = Cancel{9};
    std::cout << apply(m) << " " << m.index() << "\n";
}
Listing 3.36. Messages in a variant. code/interviews-3/03-cpp/cpp/msg_variant.cpp
Answer

Answer to Question 3.38.

It prints 300 2 24, 1, bad access and 0 1. A variant holds its alternative in place, with no allocation: 24 bytes here, for the largest alternative of 16 bytes aligned to 8, plus the index. get_if returns null for the wrong type, and get throws.

Compared with a base class and three derived classes, there is no heap and no pointer to chase, so a vector of messages is contiguous. std::visit makes the compiler check that every type is handled, since Overload fails to compile if one is missing. So adding a type breaks every visitor until it is handled, which is either what you want or a cost, depending on who owns the code. Adding an operation is easy, as it is a new visitor. With inheritance it is the other way round: a new type is easy and a new operation touches every class. A closed set of known messages suits the variant, and an open set, say plug-in strategies, suits inheritance.

std::visit dispatches on the index, which need not be cheaper than a virtual call, so the gain to claim is in memory and layout. We timed nothing here.

Yes, it can hold nothing. Changing the type destroys the old value before building the new one, and if that throws, nothing remains. That is valueless_by_exception(), and index() returns variant_npos, as the test shows when Fragile has a std::string member. libstdc++ avoids this for a small trivially copyable type, by building a temporary first: with an int member in place of the string, the same program prints 0 0.

Classic wrong answer. “A variant cannot be empty.” It can, only in that one failure state, and std::visit on it throws.

Question 3.39 ★★ QD, Dev, LLE • any • junior, experienced

You want a std::unordered_map keyed by a struct of a symbol and a venue. What two things must you supply, and what happens if you supply neither? Listing 3.37 fills it with 100 symbols on 50 venues each, under two hash functions. With libstdc++, what is the largest bucket under the weak hash, and roughly how large is it under the mixed one? Does the weak hash give wrong answers? What does it cost?

#include <cstddef>
#include <functional>
#include <iostream>
#include <unordered_map>

struct Key {
    int symbol;
    int venue;
    bool operator==(const Key&) const = default;
};

struct Weak {
    std::size_t operator()(const Key& k) const { return k.symbol; }
};

struct Mixed {
    std::size_t operator()(const Key& k) const {
        std::size_t h = std::hash<long>{}((long(k.symbol) << 32) | unsigned(k.venue));
        return h * 0x9E3779B97F4A7C15ull;
    }
};

template <class H>
std::size_t worst_bucket() {
    std::unordered_map<Key, int, H> m;
    for (int s = 0; s < 100; ++s)
        for (int v = 0; v < 50; ++v) m[Key{s, v}] = v;
    std::size_t worst = 0;
    for (std::size_t b = 0; b < m.bucket_count(); ++b)
        worst = std::max(worst, m.bucket_size(b));
    return worst;
}

int main() {
    std::cout << worst_bucket<Weak>() << " " << worst_bucket<Mixed>() << "\n";
}
Listing 3.37. A hash for a struct. code/interviews-3/03-cpp/cpp/key_hash.cpp
Answer

Answer to Question 3.39.

You must supply a hash and an equality test, and without a hash the map does not compile. The largest bucket holds 50 entries under the weak hash and, in the run here, 4 under the mixed one. The weak hash gives correct answers, and costs time: every lookup walks a chain of up to 50 keys.

Equality is needed because different keys can share a hash, and the map must tell them apart. C++20 can generate it with = default. A hash may be passed as a template argument, as here, or as a specialisation of std::hash<Key>.

Weak uses the symbol only, so all 50 venues of a symbol have equal hashes, and fall in one bucket whatever the table’s size. The map still finds the right entry by comparing keys. So a broken hash is a performance bug and never a correctness bug, as long as equal keys hash the same. That is the one hard rule: if two equal keys hashed differently, lookups would fail.

Mixed packs both fields into 64 bits, and multiplies by an odd constant. libstdc++ takes the hash modulo a prime bucket count, so it would do tolerably on the packed value alone, but a table reduced with a mask would use only low bits, and multiplying does not help there, since it moves information only upward: the low bits of the product still depend only on the venue. Such a table must take the top bits of the product, or first xor the high bits down.

The worst case in the limit is adversarial: if an attacker can choose keys with one hash, every operation becomes linear.

Classic wrong answer. h(symbol) ^ h(venue). XOR is symmetric, and equal fields cancel, so (1, 2) and (2, 1) collide, and (5, 5) hashes to 0.

Question 3.40 ★★ QD, Dev • any • junior

Value types, seen from another language. With .NET 8, what four lines does Listing 3.38 print? Fill is a C# struct. What is boxing, when does it happen here, and why does the Add through the interface not change a? Listing 3.39 is the C++ version, which prints 3 1 9: what does C++ have instead of boxing?

var a = new Fill { Qty = 1 };
object box = a;
a.Qty = 2;
Console.WriteLine($"{a.Qty} {((Fill)box).Qty}");

ICounter c = a;
c.Add();
Console.WriteLine($"{a.Qty} {((Fill)c).Qty}");

var list = new List<Fill> { a };
Fill copy = list[0];
copy.Qty = 9;
Console.WriteLine(list[0].Qty);

object o1 = 5, o2 = 5;
Console.WriteLine($"{ReferenceEquals(o1, o2)} {o1.Equals(o2)}");

interface ICounter { void Add(); }

struct Fill : ICounter
{
    public int Qty;
    public void Add() { Qty++; }
}
Listing 3.38. A struct in C#. code/interviews-3/03-cpp/csharp/Boxing/Program.cs
#include <iostream>
#include <vector>

struct Fill { int qty; };

int main() {
    Fill a{1};
    Fill copy = a;
    a.qty = 2;
    std::vector<Fill> list{a};
    list[0].qty = 9;
    Fill& ref = a;
    ref.qty = 3;
    std::cout << a.qty << " " << copy.qty << " " << list[0].qty << "\n";
}
Listing 3.39. The same in C++. code/interviews-3/03-cpp/cpp/value_cpp.cpp
Answer

Answer to Question 3.40.

It prints 2 1, 2 3, 2 and False True. Boxing is copying a value type into a new object on the heap, so it can be held through object or an interface. C# does it in object box = a, in ICounter c = a, and for each of o1 and o2.

So box holds a copy with Qty 1, and the later change to a does not reach it. c.Add() changes the box, so a stays 2, while the box reads 3. The trap is the method looking like it changes a. Reading a list element gives a copy too, so the third line shows the list unchanged. And two boxes of 5 are two objects, which is the last line: not the same reference, but equal values.

Each box is also an allocation for the collector to clean up, which is why a hot path in C# avoids boxing, by using generics, where List<Fill> stores the values inline, and constrained calls.

C++ has no boxing, as it has no common base type and no implicit heap object. An object is a value unless you ask otherwise, a copy is always visible as a copy, and reference semantics are explicit: Fill& ref or a pointer. list[0] returns a reference, so there, in contrast to C#, writing through it changes the element. When C++ needs to hold different types behind one interface, it says so with a pointer, a std::any or a std::variant.

Classic wrong answer. “A C# struct and a C++ struct are the same idea.” A C++ struct differs from a class only by default access. In C#, the keyword picks value or reference semantics.

Question 3.41 ★★★ QD, Dev, LLE • any • junior, experienced

Item has a copy constructor and a noexcept move constructor, and each adds one to Item::moves. With libstdc++, what two lines does Listing 3.40 print? Why does a vector grow by a factor, when growing by 16 elements each time would waste less memory? Give the total number of moves for 1000 insertions under each policy. Why might a library choose a factor below 2? Then Listing 3.41: what does v.push_back(v[0]) do, and how must the real one avoid it?

int main() {
    std::vector<Item> v;
    std::size_t last = 0;
    for (int i = 0; i < 1000; ++i) {
        v.emplace_back();
        if (v.capacity() != last) {
            last = v.capacity();
            std::cout << last << " ";
        }
    }
    std::cout << "\n" << Item::moves << "\n";
}
Listing 3.40. Watching the capacity. code/interviews-3/03-cpp/cpp/growth.cpp
template <class T>
class MiniVec {
    T* d_ = nullptr;
    std::size_t n_ = 0, cap_ = 0;

    void grow() {
        std::size_t c = cap_ ? 2 * cap_ : 1;
        T* fresh = new T[c];
        for (std::size_t i = 0; i < n_; ++i) fresh[i] = std::move(d_[i]);
        delete[] d_;
        d_ = fresh;
        cap_ = c;
    }

public:
    MiniVec() = default;
    MiniVec(const MiniVec&) = delete;
    ~MiniVec() { delete[] d_; }
    void push_back(const T& x) {
        if (n_ == cap_) grow();
        d_[n_++] = x;
    }
    T& operator[](std::size_t i) { return d_[i]; }
    std::size_t size() const { return n_; }
};

int main() {
    MiniVec<std::string> v;
    v.push_back(std::string(40, 'x'));
    v.push_back(v[0]);
    std::cout << v.size() << " " << v[1].size() << "\n";
}
Listing 3.41. A first attempt at a vector. code/interviews-3/03-cpp/cpp/naive_vec.cpp
Answer

Answer to Question 3.41.

Listing 3.40 prints 1 2 4 8 16 32 64 128 256 512 1024 and then 1023. For 1000 insertions, doubling makes 1023 moves, and adding 16 each time makes 31 248, and that gap widens with nn. v.push_back(v[0]) in the naive vector reads freed memory, which AddressSanitizer reports as heap-use-after-free.

Doubling moves 1+2+⋯+512=10231 + 2 + \dots + 512 = 1023 elements, about one per insertion, so each insertion costs a constant number of moves on average. A fixed increment kk moves k+2k+…k + 2k + \dots, about n2/2kn^2 / 2k in all, which is quadratic: our count roughly quadruples from 1000 to 2000 insertions. The increment saves memory and loses time.

A factor ff makes about n/(f−1)n / (f - 1) moves, so 1.5 makes twice as many as 2, and a factor near 1 becomes the fixed-increment case. The gain of a smaller factor is reuse. With doubling, each new block is larger than all the freed ones together. With 1.5, the blocks are 1,1.5,2.25,3.38,5.06,7.591, 1.5, 2.25, 3.38, 5.06, 7.59, and the sixth fits in the space of the first four, which total 8.13, if the allocator joins neighbouring free blocks. Below the golden ratio, this reuse eventually becomes possible.

In the naive code, x refers into the old block, and grow deletes that block before x is read. So the real one builds the new element in the new block first, and only then moves the old ones and frees them.

Classic wrong answer. “Growth is O(n)O(n) per insertion, as elements are copied.” That is the worst single step. The average over all insertions is constant.

Follow-up. What else is wrong with new T[c] here? It needs a default constructor, and builds cc objects that nobody asked for.

Question 3.42 ★★★ QD, Dev, LLE • any • experienced

Write a simplified std::optional<T> in C++20, with emplace, reset, a conversion to bool, operator*, and copy and move. T need not have a default constructor. Why can it not simply have a member T value? What must the storage look like? At which moment do you set the flag, and why? Listing 3.42 places a double at an odd address. x86-64 loads it without complaint, so is the program correct?

int main() {
    alignas(double) char buf[32];
    double* p = ::new (static_cast<void*>(buf + 1)) double(1.5);
    std::cout << *p << "\n";
}
Listing 3.42. An object at the wrong address. code/interviews-3/03-cpp/cpp/misaligned.cpp
Answer

Answer to Question 3.42.

A member T value would have to be built whenever the optional is, which needs a default constructor and an object that nobody asked for. So the storage is raw bytes of T’s size and alignment, and the object appears through placement new. Listing 3.56 sets the flag only after the constructor succeeds. The misaligned program is not correct, even though it runs.

template <class T>
class Opt {
    alignas(T) unsigned char buf_[sizeof(T)];
    bool has_ = false;

    T* ptr() { return std::launder(reinterpret_cast<T*>(buf_)); }
    const T* ptr() const { return std::launder(reinterpret_cast<const T*>(buf_)); }

public:
    Opt() noexcept {}
    ~Opt() { reset(); }
    Opt(const Opt& o) { if (o.has_) emplace(*o.ptr()); }
    Opt(Opt&& o) noexcept(noexcept(T(std::declval<T&&>()))) {
        if (o.has_) emplace(std::move(*o.ptr()));
    }
    Opt& operator=(Opt o) {                      // copy made first, so self-assign is safe
        reset();
        if (o.has_) emplace(std::move(*o.ptr()));
        return *this;
    }
    template <class... A>
    T& emplace(A&&... a) {
        reset();
        T* p = ::new (static_cast<void*>(buf_)) T(std::forward<A>(a)...);
        has_ = true;                             // only after the constructor succeeds
        return *p;
    }
    void reset() noexcept {
        if (has_) ptr()->~T();
        has_ = false;
    }
    explicit operator bool() const noexcept { return has_; }
    T& operator*() { return *ptr(); }
    const T& operator*() const { return *ptr(); }
};
Listing 3.56. A small optional. code/interviews-3/03-cpp/cpp/opt.hpp

alignas(T) matters as an array of unsigned char has alignment 1 by itself. If the flag were set first and the constructor threw, the destructor would destroy an object that never existed. A test with that order fails, and the listing’s order passes against std::optional on 20 000 random operations, under AddressSanitizer, with a throwing constructor among them and a 64-byte-aligned type.

std::launder is there since a pointer cast from the byte array does not by itself point at the object made inside it. Assignment takes its argument by value, so self-assignment is safe, as the copy exists before reset.

If emplace throws, the old value is already gone and the optional is empty, which is valid state. That is the basic guarantee, and not the strong one.

As for the misaligned double: UBSan reports store to misaligned address and load of misaligned address, and the program still prints 1.5. An object must be at a suitably aligned address, so this is undefined. x86-64 tolerates plain moves, but the compiler may vectorise the access with instructions that assume alignment, and other architectures may trap.

Classic wrong answer. “char buf[sizeof(T)] is enough.” It is the right size at the wrong alignment.

Question 3.43 ★★★ QD, Dev, LLE • crypto, market maker, hedge fund • experienced

A trading system creates and destroys many orders of one type, and must not touch the heap once running. In C++20, write a fixed-size pool Pool<T, N>: create(args...) returns a T*, or nullptr when full, and destroy(p) takes it back, both in constant time. Where do you keep the list of free slots? What should happen if T’s constructor throws? What mistakes does your pool not catch?

Answer

Answer to Question 3.43.

Keep the free list inside the free slots themselves: each one is a union of a next pointer and the bytes of a T, so a free slot’s memory holds the link, and no extra memory is needed. Listing 3.57 does that. create pops the head, and destroy pushes it back.

template <class T, std::size_t N>
class Pool {
    union Slot {
        Slot* next;
        alignas(T) unsigned char bytes[sizeof(T)];
    };
    Slot slots_[N];
    Slot* free_ = slots_;

public:
    Pool() {
        for (std::size_t i = 0; i + 1 < N; ++i) slots_[i].next = &slots_[i + 1];
        slots_[N - 1].next = nullptr;
    }
    Pool(const Pool&) = delete;
    Pool& operator=(const Pool&) = delete;

    template <class... A>
    T* create(A&&... a) {
        if (!free_) return nullptr;
        Slot* s = free_;
        free_ = s->next;                         // read now: the object overwrites it
        try {
            return ::new (static_cast<void*>(s->bytes)) T(std::forward<A>(a)...);
        } catch (...) {
            s->next = free_;                     // a failed constructor loses no slot
            free_ = s;
            throw;
        }
    }
    void destroy(T* p) noexcept {
        p->~T();
        Slot* s = reinterpret_cast<Slot*>(p);
        s->next = free_;
        free_ = s;
    }
};
Listing 3.57. A fixed pool with an intrusive free list. code/interviews-3/03-cpp/cpp/pool.hpp

There is a trap in create. The new object is built over the bytes that hold next, so next must be read before the constructor runs. Our test builds the version that reads it afterwards: the id field overwrites the link with 0, and the pool reports itself full with 15 slots still free.

If the constructor throws, nothing was created, and the slot goes back on the list, so an exception loses no capacity. The test throws from one creation in about 8, and the live count and the exhaustion test still agree with a model after 100 000 random operations.

It makes no call to operator new in that run, and each slot is aligned for T, which the test checks with a 64-byte type. A slot is at least the size of a pointer, however small T is. The most recently freed slot comes back first, and is the one most likely to still be in the cache.

It does not catch these: destroying a pointer twice, which corrupts the list with a cycle; using an object after destroy; passing a pointer from another pool. And it is for one thread. A second thread needs a lock or a lock-free list, where popping meets the ABA problem. A debug build could keep a bitmap of live slots to catch the first of these.

Classic wrong answer. “T* p = new T[N] and a stack of pointers.” That constructs all NN objects at the start and needs a default constructor, so create would have nothing left to construct.

Question 3.44 ★★★ QD, Dev, LLE • any • experienced

In C++20, write a bump arena over a fixed buffer, and an allocator type that lets std::vector<int> and std::map<int, double> take all their memory from it. What must an allocator provide at the minimum? Why does it need a converting constructor from an allocator of another type, when your code only ever names pair? What does deallocate do, and what happens to the old blocks as a vector grows?

Answer

Answer to Question 3.44.

An allocator needs only value_type, allocate, deallocate, equality, and a converting constructor from the same allocator for another type. Listing 3.58 is the arena and that allocator. deallocate does nothing, as a bump arena frees everything at once, by dropping the buffer.

class Arena {
    void* cur_;
    std::size_t left_;

public:
    Arena(void* buf, std::size_t n) : cur_(buf), left_(n) {}
    void* take(std::size_t n, std::size_t align) {
        if (!std::align(align, n, cur_, left_)) throw std::bad_alloc{};
        void* p = cur_;
        cur_ = static_cast<char*>(cur_) + n;
        left_ -= n;
        return p;
    }
    std::size_t left() const { return left_; }
};

template <class T>
struct ArenaAlloc {
    using value_type = T;
    Arena* arena;
    explicit ArenaAlloc(Arena& a) noexcept : arena(&a) {}
    template <class U>
    ArenaAlloc(const ArenaAlloc<U>& o) noexcept : arena(o.arena) {}
    T* allocate(std::size_t n) {
        return static_cast<T*>(arena->take(n * sizeof(T), alignof(T)));
    }
    void deallocate(T*, std::size_t) noexcept {}
    template <class U>
    bool operator==(const ArenaAlloc<U>& o) const noexcept { return arena == o.arena; }
};
Listing 3.58. A bump arena and an allocator over it. code/interviews-3/03-cpp/cpp/arena_alloc.hpp

std::align advances the pointer past any padding, and fails when the rest of the buffer is too small, at which point we throw. The test takes 1 byte and then an aligned double, and gets an aligned address.

The converting constructor exists because of what a container allocates. You name pair<const int, double>, but a std::map allocates tree nodes, a private type that you never see. It rebinds your allocator to that node type and constructs one from yours, which the constructor makes possible. Equality says whether storage from one can be released through the other, so here it compares the arena.

The allocator holds a pointer to the arena and not the arena itself, as allocators get copied, and all copies must draw on one buffer.

The old blocks are simply lost. A vector growing to 100 ints takes 1, 2, 4 up to 128, which is 255 ints of arena for 100 elements. So call reserve when you know the size, or use a pool that reuses memory.

The test fills a vector and a map from the buffer, makes no call to global operator new, gets the right sums back, and sees bad_alloc when a request is too large. The buffer must outlive all containers using it, which the type does not enforce.

Classic wrong answer. “Containers call allocate for the elements I named, so one type is enough.” That is true of a vector, not of a node-based container.

Question 3.45 ★★★ QD, Dev, LLE • any • experienced

With g++ 11.4 and libstdc++, what three lines does Listing 3.43 print? The program replaces the global operator new so that it adds 1 to news. Explain 1020 and 400. Why do the strings in the second block use the buffer, though nobody passed it to them? What does this design gain over an allocator passed as a template argument, and what does it cost?

struct Counting : pmr::memory_resource {
    pmr::memory_resource* up;
    long bytes = 0;
    explicit Counting(pmr::memory_resource* u) : up(u) {}
    void* do_allocate(std::size_t n, std::size_t a) override {
        bytes += n;
        return up->allocate(n, a);
    }
    void do_deallocate(void*, std::size_t, std::size_t) override {}
    bool do_is_equal(const memory_resource& o) const noexcept override {
        return this == &o;
    }
};
long used(bool reserve) {
    alignas(64) char buf[4096];
    pmr::monotonic_buffer_resource mono(buf, sizeof buf, pmr::null_memory_resource());
    Counting count(&mono);
    pmr::vector<int> v(&count);
    if (reserve) v.reserve(100);
    for (int i = 0; i < 100; ++i) v.push_back(i);
    return count.bytes;
}
int main() {
    std::cout << used(false) << " " << used(true) << "\n";
    long before = news;
    {
        alignas(64) char buf[8192];
        pmr::monotonic_buffer_resource mono(buf, sizeof buf, pmr::null_memory_resource());
        pmr::vector<pmr::string> rows(&mono);
        rows.emplace_back(100, 'x');
        rows.emplace_back(100, 'y');
        bool same = rows[0].get_allocator().resource() == &mono;
        std::cout << same << " " << news - before << "\n";
    }
    alignas(64) char tiny[64];
    pmr::monotonic_buffer_resource mono(tiny, sizeof tiny, pmr::null_memory_resource());
    pmr::vector<int> v(&mono);
    try { v.resize(1000); } catch (const std::bad_alloc&) { std::cout << "bad_alloc\n"; }
}
Listing 3.43. Polymorphic memory resources. Here pmr is an alias of std::pmr. code/interviews-3/03-cpp/cpp/pmr_use.cpp
Answer

Answer to Question 3.45.

It prints 1020 400, then 1 0, then bad_alloc. Without reserve, the vector asks for 1, 2, 4 up to 128 ints, which is 255 ints or 1020 bytes, and the arena never reuses any. With reserve(100), it asks once, for 400 bytes.

The second line shows that a string’s resource is the buffer, and that global new was called zero times. A pmr::vector constructing a pmr::string passes its own allocator down, so each element uses the same resource. This is called uses-allocator construction, and it is the reason nested containers are so easy here.

The last line comes from the resource’s upstream. When the 64-byte buffer runs out, the monotonic resource asks upstream for more, and null_memory_resource always throws. That turns “this path must not reach the heap” into a checkable failure. With the default upstream, it would quietly go to the heap instead.

The gain over a template allocator is in the type. std::vector<int, ArenaAlloc<int>> and std::vector<int> are different types, so a function must be a template to take both. Every pmr::vector<int> is one type, whatever its resource, so the choice is made at run time.

The cost is a pointer in each container, and a virtual call at each allocation, which matters little when allocations are rare. There is also a trap. Copy-constructing a pmr::vector does not inherit the source’s resource: the copy uses the default one, which our test shows, so a copy can reach the heap unnoticed.

Classic wrong answer. “The vector frees its old blocks into the buffer.” do_deallocate on a monotonic resource does nothing, and memory only returns when the resource is released or destroyed.

Question 3.46 ★★★ QD, Dev, LLE • any • junior, experienced

In Listing 3.44, get stands for an allocation that can throw. Find two separate faults in operator=. For each, say exactly what happens: a = a, and a = b when get throws. Then rewrite the operator so that both are correct. What does your version cost, and when might you choose not to pay it?

#include <algorithm>
#include <cstddef>
#include <new>

static bool fail_next = false;
int* get(std::size_t n) {
    if (fail_next) throw std::bad_alloc{};
    return new int[n]{};
}

class Buf {
    std::size_t n_;
    int* d_;

public:
    explicit Buf(std::size_t n) : n_(n), d_(get(n)) {}
    Buf(const Buf& o) : n_(o.n_), d_(get(o.n_)) { std::copy(o.d_, o.d_ + n_, d_); }
    ~Buf() { delete[] d_; }
    Buf& operator=(const Buf& o) {
        delete[] d_;
        n_ = o.n_;
        d_ = get(n_);
        std::copy(o.d_, o.d_ + n_, d_);
        return *this;
    }
    int& operator[](std::size_t i) { return d_[i]; }
};
Listing 3.44. An assignment with two faults. code/interviews-3/03-cpp/cpp/assign_bad.cpp
Answer

Answer to Question 3.46.

a = a silently wipes the data, and the program prints 0 rather than 7. a = b with a throwing get leaves d_ pointing at freed memory, so the destructor frees it again, and AddressSanitizer reports attempting double-free. Listing 3.59 is correct in both cases, through copy and swap.

class Buf {
    std::size_t n_;
    int* d_;

public:
    explicit Buf(std::size_t n) : n_(n), d_(get(n)) {}
    Buf(const Buf& o) : n_(o.n_), d_(get(o.n_)) { std::copy(o.d_, o.d_ + n_, d_); }
    ~Buf() { delete[] d_; }
    friend void swap(Buf& a, Buf& b) noexcept {
        std::swap(a.n_, b.n_);
        std::swap(a.d_, b.d_);
    }
    Buf& operator=(const Buf& o) {
        Buf copy(o);                             // may throw; *this is untouched
        swap(*this, copy);                       // cannot throw
        return *this;
    }
    int& operator[](std::size_t i) { return d_[i]; }
};
Listing 3.59. Copy, then swap. code/interviews-3/03-cpp/cpp/assign_good.cpp

On self-assignment, o and *this are one object. delete[] d_ frees the data, then d_ = get(n_) installs a fresh zeroed block, and std::copy copies that block onto itself. No freed memory is read, so AddressSanitizer stays silent, and the data is just gone. That self-copy also breaks std::copy’s precondition, so formally it is undefined; on this library it is a harmless memmove. An if (this != &o) would fix this fault and leave the other in place.

The second fault is the order: the old state is destroyed before the new one is known to exist. If get throws, d_ still holds the deleted pointer.

In the new version, the copy is made first, while *this is untouched, and that is the only line that can throw. The swap cannot, and the destructor of copy frees the old data. Self-assignment needs no check. The test prints 7, caught 7 and 8: the value survives self-assignment, and a failed assignment leaves it unchanged.

The cost is an allocation on every assignment, even when the existing block is big enough. A std::vector avoids that by copying into its existing capacity, and so gives up the strong guarantee for its copy assignment: if an element’s assignment throws partway, the vector holds a mix. On a hot path, that is the right trade, provided the caller can live with it.

Classic wrong answer. “Add if (this == &o) return *this; and it is fixed.” That handles a case that barely happens, and leaves the failure that does.

Question 3.47 ★★★ QD, Dev, LLE • crypto, market maker, hedge fund • experienced

With g++ 11.4, -std=c++20 -O2, x86-64, what two lines does Listing 3.45 print? Why is the first line not 0, when the store through b really wrote zeros over x? What does -fno-strict-aliasing change? You are parsing a 4-byte price from a packet in a const char* buffer. How do you read it correctly, and what does that cost?

#include <bit>
#include <cstdint>
#include <iostream>

[[gnu::noinline]] int store_both(int* a, float* b) {
    *a = 1;
    *b = 0.0f;
    return *a;
}

int main() {
    int x = 0;
    std::cout << store_both(&x, reinterpret_cast<float*>(&x)) << "\n";
    std::cout << std::hex << std::bit_cast<std::uint32_t>(1.0f) << "\n";
}
Listing 3.45. One address, two types. code/interviews-3/03-cpp/cpp/aliasing.cpp
Answer

Answer to Question 3.47.

It prints 1, then 3f800000. An int* and a float* are assumed not to point at the same object, so the compiler returns 1 without reading *a again. With -fno-strict-aliasing, it makes no such assumption, reloads, and the first line becomes 0.

The rule: an object may be read only through its own type, a signed or unsigned version of it, or a char, unsigned char or std::byte. Reading an int through a float is undefined, so main is at fault here, and not the optimiser. Assumptions like this one let the compiler keep values in registers across unrelated stores, so a loop writing through a float* need not reload an int each time round.

The second line shows the legal way to see a float’s bits: std::bit_cast gives a new value with the same bits, here 0x3f800000 for 1.0.

For a packet, *reinterpret_cast<const std::uint32_t*>(p) breaks two rules: aliasing, and alignment, since a field in a packet may sit at any offset. The right way is to declare a std::uint32_t v and std::memcpy(&v, p, 4). It looks slow, and costs nothing here: at -O2, our test finds a single movl (%rdi), %eax, and no call. Convert the byte order afterwards, if the wire format needs it.

Many large code bases compile with -fno-strict-aliasing as a safety net, which is a choice, and leaves the program non-portable.

Classic wrong answer. “A union of the two is the legal way.” That is allowed in C, and it is undefined in C++, though g++ documents it as an extension.

Follow-up. And casting a struct over the buffer? The same two rules are broken, and the layout has padding to think about too. Copy the fields, or memcpy into a struct whose layout you have checked.

Question 3.48 ★★★ QD, Dev, LLE • any • experienced

Listing 3.46 has two functions that differ only in the order of two lines. With g++ 11.4, -O2, x86-64, what does each one compile to? Why was a null check removed from one of them? What happens on read_bad(nullptr)? Which option keeps the check, and is that the right fix?

int read_bad(int* p) {
    int v = *p;
    if (!p) return -1;
    return v;
}

int read_good(int* p) {
    if (!p) return -1;
    return *p;
}
Listing 3.46. A check placed after the use. code/interviews-3/03-cpp/cpp/nullcheck.cpp
Answer

Answer to Question 3.48.

read_bad compiles to one load and a return, and has no check at all. read_good tests the pointer, and has a branch to return −1-1. The dereference on the first line tells the compiler that p is not null, because a null dereference is undefined, so if (!p) can never be true, and it is deleted.

The compiler makes no special effort here. It propagates a fact it learned, as it does everywhere, and then removes dead code. Each step is valid, and the result shocks only because the check was the author’s safety net. A warning is not promised, and our build with -Wall -Wextra gives none.

On read_bad(nullptr) on this platform, the load reads address 0, and the process dies with a segmentation fault. So the check was useless even before it was removed, as the crash comes on the line above. However, that is not guaranteed either: if v were unused, the load could vanish too, and the program would carry on in a state its author believed impossible.

-fno-delete-null-pointer-checks keeps the check, and our test finds the test instruction back in the assembly. On a system where address 0 can be mapped, that matters. As a fix for this code, it is the wrong answer: the line order is the bug, and the flag only hides it for one compiler. Move the check to before the use, as read_good does.

The general lesson is that undefined behaviour reaches backward and forward. An operation in the future can change what the compiler assumes about the past, so “it only crashes if we get there” is not reliable.

Classic wrong answer. “The optimiser removed a check, so that is a compiler bug.” The standard lets it assume the absence of undefined behaviour.

Question 3.49 ★★★ QD, Dev, LLE • any • junior, experienced

Listing 3.47 compiles with no warning under g++ 11.4, -Wall -Wextra. Run it with no arguments, then with three. Under AddressSanitizer, what happens each time, and why does the number of arguments matter? Why does std::max return a reference at all? Why does std::max(1, 2.0) not compile? Give the fix for this listing.

#include <algorithm>
#include <iostream>

int main(int argc, char**) {
    const int& m = std::max(argc, 2);
    std::cout << m << "\n";
}
Listing 3.47. The larger of two. code/interviews-3/03-cpp/cpp/max_dangle.cpp
Answer

Answer to Question 3.49.

With no arguments, AddressSanitizer reports stack-use-after-scope at line 6. With three, the program runs clean and prints 4. std::max returns a reference to whichever argument wins. With argc equal to 1, that is the temporary holding 2, which is gone by the end of line 5. With argc equal to 4, it is argc itself, which is still alive.

So the bug depends on the data. A test whose argc is larger than 2 passes, and nothing warns that the other case dangles.

The reference binding m extends no lifetime. The temporary is bound to the function’s parameter, and it lives until the end of the full expression. What comes back from the function is a new reference, and the compiler does not know that it may be this temporary.

std::max returns a reference so that it does not force a copy. It works for a type that cannot be copied or is costly to copy, and its result can be used in place, within the same expression. That is safe as long as the result is not kept.

std::max(1, 2.0) does not compile, since T is deduced from both arguments, and comes out as int and as double. No conversion is considered during deduction. You write std::max<double>(1, 2.0).

The fix here is to hold a value: int m = std::max(argc, 2);. That copies while the temporary is alive, and for an int the copy is nothing. The same holds for std::min and std::clamp.

Classic wrong answer. “A const reference extends the temporary, so this is safe.” The extension applies only to a temporary bound directly by that reference.

Follow-up. And std::max(a, b).size() on two strings? Fine: the call and the use are in one full expression, so nothing has died yet.

Question 3.50 ★★★ QD, Dev, LLE • crypto, market maker, hedge fund • experienced

A live configuration is held in a std::shared_ptr: one thread replaces it, and another copies it to read it. shared_ptr’s reference count is atomic. In Listing 3.48, run with argument A under ThreadSanitizer, is the program race-free? What can actually go wrong? What does mode B change? Four threads each hold their own copy of one shared_ptr and copy it in a loop: is that a race? On g++ 11.4, can you use std::atomic<std::shared_ptr<int>>?

#include <iostream>
#include <memory>
#include <thread>

std::shared_ptr<int> config = std::make_shared<int>(1);

int main(int, char** argv) {
    long sum = 0;
    const char mode = argv[1][0];
    std::thread writer([&] {
        for (int i = 0; i < 2000; ++i) {
            if (mode == 'A') config = std::make_shared<int>(i);
            if (mode == 'B') std::atomic_store(&config, std::make_shared<int>(i));
        }
    });
    std::thread reader([&] {
        for (int i = 0; i < 2000; ++i) {
            if (mode == 'A') { auto p = config; sum += *p; }
            if (mode == 'B') { auto p = std::atomic_load(&config); sum += *p; }
        }
    });
    writer.join();
    reader.join();
    std::cout << (sum >= 0) << "\n";
}
Listing 3.48. Replacing a shared pointer under a reader. code/interviews-3/03-cpp/cpp/shared_race.cpp
Answer

Answer to Question 3.50.

Mode A is a data race, and ThreadSanitizer reports it. The count inside the control block is atomic, but the shared_ptr object is not: it is two pointers, and one thread writes them as another reads them. Mode B goes through std::atomic_store and std::atomic_load, and is clean.

What can go wrong is concrete. The reader loads the control-block pointer and is then paused. The writer replaces config, the old count reaches zero, and the block is freed. The reader wakes, increments a count in freed memory, and holds an object that no longer exists. It can also see the new object pointer with the old block, since the two are not read together.

The rule is the usual one for a type: concurrent reads of one object are fine, as is any access to distinct objects, and a write to one object alongside anything else on it is a race. The atomic count is what makes distinct objects safe even when they share an owned object. So four threads with their own copies are not a race, and ThreadSanitizer finds none in Listing 3.60.

int main() {
    auto root = std::make_shared<int>(5);
    std::vector<std::thread> ts;
    for (int k = 0; k < 4; ++k)
        ts.emplace_back([p = root] {             // each thread owns its own copy
            for (int i = 0; i < 2000; ++i) {
                auto q = p;
                (void)q;
            }
        });
    for (auto& t : ts) t.join();
    std::cout << root.use_count() << " " << *root << "\n";
}
Listing 3.60. Each thread has its own copy. code/interviews-3/03-cpp/cpp/shared_copies.cpp

std::atomic<std::shared_ptr<int>> is the C++20 way, but libstdc++ 11 does not have it: g++ 11.4 stops at a static_assert, as shared_ptr is not trivially copyable. It arrived in GCC 12. So we used the older free functions, which libstdc++ implements by taking one of a small set of mutexes, as its header shows. These functions are deprecated in C++20.

So this path takes a lock, and even a plain copy of a shared_ptr is an atomic read-modify-write on a line that all readers share. A hot reader would rather keep its own copy, and refresh it now and then.

Classic wrong answer. “shared_ptr is thread-safe.” Its count is. The object holding the two pointers is not.

Question 3.51 ★★★ QD, Dev, LLE • any • junior, experienced

In C++20, write a bounded blocking queue for several producers and consumers, with push, pop and close. push blocks while full and returns false once closed. pop blocks while empty and returns an empty std::optional once closed and drained. Why two condition variables? Why does each wait take a predicate? What must close do so that nobody sleeps forever? How would you test it?

Answer

Answer to Question 3.51.

Listing 3.61 is one answer. There are two condition variables, so that a push wakes only consumers and a pop only producers, and each wait has a predicate, as a wait can return when the condition does not hold. close sets the flag under the lock and then wakes everyone on both.

template <class T>
class BoundedQueue {
    std::mutex m_;
    std::condition_variable not_empty_, not_full_;
    std::deque<T> q_;
    const std::size_t cap_;
    bool closed_ = false;

public:
    explicit BoundedQueue(std::size_t cap) : cap_(cap) {}

    bool push(T x) {
        std::unique_lock lock(m_);
        not_full_.wait(lock, [&] { return q_.size() < cap_ || closed_; });
        if (closed_) return false;
        q_.push_back(std::move(x));
        lock.unlock();
        not_empty_.notify_one();
        return true;
    }
    std::optional<T> pop() {
        std::unique_lock lock(m_);
        not_empty_.wait(lock, [&] { return !q_.empty() || closed_; });
        if (q_.empty()) return std::nullopt;     // closed and drained
        T x = std::move(q_.front());
        q_.pop_front();
        lock.unlock();
        not_full_.notify_one();
        return x;
    }
    void close() {
        { std::lock_guard lock(m_); closed_ = true; }
        not_empty_.notify_all();
        not_full_.notify_all();
    }
};
Listing 3.61. A bounded queue with close. code/interviews-3/03-cpp/cpp/bq.hpp

With one condition variable, a push could wake another producer, which finds the queue full and sleeps again, and if notify_one chose that one, the wake-up is lost, and the waiting consumer sleeps on. Using notify_all would fix that at the price of many pointless wake-ups.

A wait can return for two reasons without a true condition: a spurious wake-up, which the standard permits, and a stolen one, in which another thread took the item before this thread got the lock. The predicate form loops and checks again.

Setting the flag under the lock is essential. Otherwise a waiter could check its predicate, see no close, and go to sleep just after the notification, and never wake. The notification itself can come after the unlock, so a woken thread does not at once block on the mutex the notifier still holds. Note that pop still returns items after close, so the queue drains, and returns nothing only when it is empty.

The test runs 3 producers and 3 consumers through a queue of capacity 4. It checks the count, 9000, and the exact sum, 27 009 000, which would catch a lost or doubled item. That runs under ThreadSanitizer. Then it blocks a producer on a full queue, and confirms that close wakes it with false, and the item already inside can still be popped. A hang would fail the test on its timeout.

Classic wrong answer. “if (q_.empty()) wait” instead of a loop. It returns with an empty queue the first time a wake-up is stolen.

Question 3.52 ★★★ QD, Dev, LLE, QR • any • junior, experienced

In C++20, write choose(n, k) for the binomial coefficient, on std::uint64_t, returning an empty std::optional exactly when the true answer does not fit in 64 bits. It must not report overflow for an answer that fits, even if a naive intermediate product would overflow. Why does the factorial formula fail, and from which nn? Prove that your intermediate values never overflow unless the answer does. What is the smallest nn at which the answer can overflow?

Answer

Answer to Question 3.52.

Listing 3.62 builds the result one factor at a time, cancelling common factors before multiplying. It agrees with an exact computation for all n≤75n \le 75, reports overflow on exactly the cases that do not fit, and the first overflow appears at n=68n = 68, for k=31k = 31.

#include <algorithm>
#include <cstdint>
#include <iostream>
#include <numeric>
#include <optional>

std::optional<std::uint64_t> choose(std::uint64_t n, std::uint64_t k) {
    if (k > n) return 0;
    k = std::min(k, n - k);
    std::uint64_t r = 1;
    for (std::uint64_t i = 1; i <= k; ++i) {
        std::uint64_t g = std::gcd(r, i);        // r * (n - k + i) / i, with the common
        std::uint64_t top = (n - k + i) / (i / g);   // factor cancelled before multiplying
        if (__builtin_mul_overflow(r / g, top, &r)) return std::nullopt;
    }
    return r;
}
Listing 3.62. A binomial coefficient that reports overflow. code/interviews-3/03-cpp/cpp/choose.cpp

The factorial formula fails at once, since 21!21! is more than 2642^{64}, though (2110)\binom{21}{10} is only 352 716.

The loop keeps r=(n−k+ii)r = \binom{n - k + i}{i} after step ii. To reach the next value, we need r×(n−k+i)/ir \times (n - k + i) / i, which is an exact division. Multiplying first could overflow when the result fits. So let g=gcd⁡(r,i)g = \gcd(r, i). Then r/gr / g and i/gi / g share no factor, so i/gi / g must divide n−k+in - k + i exactly. That gives (r/g)×((n−k+i)/(i/g))(r / g) \times ((n - k + i) / (i / g)), a product of two integers equal to the next binomial, with nothing larger on the way.

Why is no overflow reported falsely? That product is exactly (n−k+ii)\binom{n - k + i}{i}, and these values grow with ii up to kk, since k≤n/2k \le n / 2 after taking the minimum. So if a step overflows, then so does (nk)\binom{n}{k}, which is the last of them.

The smallest nn is 68, as (6733)\binom{67}{33} is about 1.42×10191.42 \times 10^{19} and fits, and (6831)\binom{68}{31} is about 2.19×10192.19 \times 10^{19}, above 264≈1.84×10192^{64} \approx 1.84 \times 10^{19}. The test checks edge cases too: k>nk > n gives 0, k=0k = 0 gives 1, and n=264−1n = 2^{64} - 1 with k=1k = 1 returns nn, and with k=2k = 2 overflows.

__builtin_mul_overflow is a g++ and Clang extension. A portable form tests a > max / b first.

Classic wrong answer. r = r * (n - k + i) / i “as the division is exact”. The division is exact, but the multiplication before it can overflow while the true answer fits.

Question 3.53 ★★★ QD, Dev, LLE • any • junior, experienced

Listing 3.49 is run three times with g++ 11.4, with the argument noexcept, then destructor, then plain. What happens each time? Why does the try block not catch the first two? Closer’s destructor has no noexcept written on it, so why does it behave like the first? Could you let a destructor throw, and when would it still end the program?

#include <iostream>
#include <stdexcept>
#include <cstring>

void promised() noexcept { throw std::runtime_error("from noexcept"); }

struct Closer {
    ~Closer() { throw std::runtime_error("from destructor"); }
};

int main(int, char** argv) {
    try {
        if (!std::strcmp(argv[1], "noexcept")) promised();
        if (!std::strcmp(argv[1], "destructor")) { Closer c; }
        if (!std::strcmp(argv[1], "plain")) throw std::runtime_error("plain");
    } catch (const std::exception& e) {
        std::cout << "caught " << e.what() << "\n";
    }
}
Listing 3.49. Three ways to throw. code/interviews-3/03-cpp/cpp/terminate.cpp
Answer

Answer to Question 3.53.

With noexcept and destructor, the program prints terminate called and the message, and dies with SIGABRT. With plain, it prints caught plain. The try block is never given the first two, as the exception cannot leave the function that promised not to throw.

noexcept is a promise enforced at the function’s edge. When the search for a handler reaches the edge of a non-throwing function, the language calls std::terminate rather than carrying on outward. The standard leaves open whether the stack is unwound first, so destructors may not have run.

Destructors are noexcept by default since C++11, without being written so, unless a member’s or a base’s destructor is declared able to throw. So ~Closer() is just as non-throwing as promised. The compiler sees this and warns with -Wterminate, which is on by default. The listing turns that off on a line not shown, to make the point.

You can allow it: ~Closer() noexcept(false). Our test builds that, and it prints caught from destructor. But if a destructor throws while another exception is already unwinding the stack, there would be two at once, and the program terminates, with no way to catch either. The test shows that too. So a destructor that can run during unwinding must not throw.

A file or socket closer should report failure another way. Offer a close() method that may throw, or return a status for a caller who cares, and let the destructor swallow and log whatever remains.

Classic wrong answer. “Wrap the code in try and catch (...) in main.” That is the code shown. Control never returns there.

Follow-up. Does noexcept make code faster? It removes some work for the caller, and it lets containers move. The second is where it matters most.

Question 3.54 ★★★ QD, Dev, LLE • crypto, market maker, hedge fund • junior, experienced

What does Listing 3.50 print? Explain how Strat<Momo> reaches Momo::signal with no virtual function. With g++ 11.4, -O2, what does the assembly of static_call contain that differs from virtual_call? Where does the speed really come from? What happens if Momo has no signal? Why can you not put a Momo and another strategy of this form into one container?

#include <iostream>
#include <memory>
#include <vector>

template <class Derived>
struct Strat {
    long on_tick(long px) { return static_cast<Derived*>(this)->signal(px) * 2; }
};
struct Momo : Strat<Momo> {
    long signal(long px) { return px + 1; }
};

struct IStrat {
    virtual long signal(long px) = 0;
    virtual ~IStrat() = default;
};
struct A : IStrat { long signal(long px) override { return px + 1; } };
struct B : IStrat { long signal(long px) override { return px - 1; } };

long static_call(Momo& s, long px) { return s.on_tick(px); }
long virtual_call(IStrat& s, long px) { return s.signal(px) * 2; }

int main() {
    Momo m;
    std::vector<std::unique_ptr<IStrat>> all;
    all.push_back(std::make_unique<A>());
    all.push_back(std::make_unique<B>());
    long sum = static_call(m, 10);
    for (auto& s : all) sum += virtual_call(*s, 10);
    std::cout << sum << "\n";
}
Listing 3.50. Static and dynamic dispatch. code/interviews-3/03-cpp/cpp/crtp.cpp
Answer

Answer to Question 3.54.

It prints 62: static_call gives (10+1)×2=22(10 + 1) \times 2 = 22, and the two virtual calls give 22 and 18. Momo derives from Strat<Momo>, so the base knows its derived type at compile time, and static_cast<Derived*>(this) is a plain, resolved call. The assembly of static_call has no call at all, while virtual_call has call *(%rax), an indirect call through the virtual table.

This is the curiously recurring template pattern. A base class template takes the derived class as its parameter. The cast is safe if the object really is a Momo, which holds as long as only Momo derives from Strat<Momo>.

The speed comes mainly from inlining. The compiler can see which function is called, so it copies in the body, and that opens up further optimisation across the call boundary. The indirect call itself is usually cheap when it is predicted well. So the difference depends on what the callee does: for a tiny function in a tight loop it can be large, and for a function that does real work it is small. We measured nothing here, so measure in your own code.

Without signal, the error appears at the line in on_tick, and only if on_tick is used, since a member of a class template is instantiated on use. A concept could give a better message.

Strat<Momo> and Strat<Other> are unrelated types, as there is no common base for a container to hold. So CRTP suits a strategy known at compile time. To choose among strategies at run time, use a virtual function, or a std::variant and std::visit. A common mix is one virtual call per message, with statically dispatched code inside it.

Classic wrong answer. “CRTP is always faster, so remove all virtual functions.” A compiler can also devirtualise when it knows the type, as with final, and a virtual call that is predicted well costs little.

Question 3.55 ★★★ QD, Dev, LLE • any • junior, experienced

Run with no arguments, what does Listing 3.51 print? Replace the comment with each of int d = twice(argc);, int e = always(argc);, constexpr int f = twice(argc); and constexpr int o = twice(2000000000);. Which compile? How do constexpr and consteval on a function differ? The last line would be undefined at run time, so why is it a compile error here?

#include <array>
#include <cstdint>
#include <iostream>

constexpr std::array<std::int64_t, 19> make_powers() {
    std::array<std::int64_t, 19> t{};
    std::int64_t x = 1;
    for (std::size_t i = 0; i < t.size(); ++i) {
        t[i] = x;
        if (i + 1 < t.size()) x *= 10;
    }
    return t;
}

constexpr auto powers = make_powers();
static_assert(powers[18] == 1'000'000'000'000'000'000);

constexpr int twice(int x) { return 2 * x; }
consteval int always(int x) { return 2 * x; }

int main(int argc, char**) {
    constexpr int a = twice(21);
    int b = twice(argc);
    int c = always(21);
    std::cout << a << " " << b << " " << c << " " << powers[3] << "\n";
    // try each line here
}
Listing 3.51. Work at compile time. code/interviews-3/03-cpp/cpp/constexpr_table.cpp
Answer

Answer to Question 3.55.

It prints 42 2 42 1000. Only int d = twice(argc); compiles of the four. A constexpr function may run at compile time or at run time, and consteval must run at compile time. A constexpr variable must have a constant initialiser.

So twice(21) initialising a constexpr variable runs in the compiler, and twice(argc) runs in the program, as d needs no constant. always(argc) fails because a consteval call must give a constant, and argc is not one. constexpr int f = twice(argc) fails since the variable demands what the argument cannot give.

The last one is the instructive one. Doubling 2 000 000 000 overflows int. At run time, that would be undefined behaviour, compiled silently. In a constant expression, the standard disqualifies any operation that would be undefined, so the compiler must diagnose it. Constant evaluation is thus a free check for undefined behaviour, for values you can compute at compile time, which is one reason to write a static_assert on a table.

The powers table is built by a loop inside the compiler, and goes into the program as read-only data. No start-up code is needed, there is no order-of-initialisation issue, and powers[3] just reads a constant. The static_assert that powers[18] is 101810^{18} checks the table at every build. Note the guard in the loop, which stops it from computing 101910^{19}: without it, the last multiply overflows, and the build fails.

The cost is compile time, and data in the binary. A table used in a hot path may also compete for cache, which a small computation does not, so a table is not always the faster choice.

Classic wrong answer. “constexpr on a function means it runs at compile time.” It only permits that. The call decides.

Follow-up. And constinit? It requires constant initialisation of a static variable, and the variable can then change, unlike constexpr.

Question 3.56 ★★★★ QD, Dev, LLE • any • experienced

In C++20, implement a Vec<T> with the destructor, copy and move, emplace_back, pop_back and operator[], and which works for a type with no default constructor. emplace_back must give the strong guarantee, and v.emplace_back(v[0]) must work when it triggers a reallocation. Why not new T[c]? In what order do the steps of a reallocation go? When do you move the old elements, and when copy them? How would you test the guarantee?

Answer

Answer to Question 3.56.

Listings 3.63 and 3.64 are one implementation. A reallocation goes: allocate the new block, build the new element in it at its final place, transfer the old elements, destroy them and free the old block, and only then update the members. The vector moves when the move cannot throw or when there is no copy, and copies otherwise.

template <class T>
class Vec {
    T* d_ = nullptr;
    std::size_t n_ = 0, cap_ = 0;
    static T* get(std::size_t c) { return std::allocator<T>{}.allocate(c); }
    void put() noexcept {
        std::destroy_n(d_, n_);
        if (d_) std::allocator<T>{}.deallocate(d_, cap_);
    }
    void transfer(T* to) {
        if constexpr (std::is_nothrow_move_constructible_v<T>
                      || !std::is_copy_constructible_v<T>)
            std::uninitialized_move_n(d_, n_, to);
        else
            std::uninitialized_copy_n(d_, n_, to);
    }

public:
    Vec() = default;
    ~Vec() { put(); }
    Vec(const Vec& o) : d_(get(o.n_)), cap_(o.n_) {
        try { std::uninitialized_copy_n(o.d_, o.n_, d_); }
        catch (...) { std::allocator<T>{}.deallocate(d_, cap_); throw; }
        n_ = o.n_;
    }
    Vec(Vec&& o) noexcept
        : d_(std::exchange(o.d_, nullptr)), n_(std::exchange(o.n_, 0)),
          cap_(std::exchange(o.cap_, 0)) {}
    Vec& operator=(Vec o) noexcept {
        std::swap(d_, o.d_); std::swap(n_, o.n_); std::swap(cap_, o.cap_);
        return *this;
    }
Listing 3.63. A vector: storage, copy, move. code/interviews-3/03-cpp/cpp/vec.hpp
    template <class... A>
    T& emplace_back(A&&... a) {
        if (n_ < cap_) {
            T* p = ::new (static_cast<void*>(d_ + n_)) T(std::forward<A>(a)...);
            ++n_;
            return *p;
        }
        std::size_t c = cap_ ? 2 * cap_ : 1;
        T* fresh = get(c);
        T* slot = fresh + n_;
        try {
            ::new (static_cast<void*>(slot)) T(std::forward<A>(a)...);   // before the old go
            try { transfer(fresh); }
            catch (...) { slot->~T(); throw; }
        } catch (...) {
            std::allocator<T>{}.deallocate(fresh, c);
            throw;
        }
        put();
        d_ = fresh;
        cap_ = c;
        ++n_;
        return *slot;
    }
    void pop_back() noexcept { d_[--n_].~T(); }
    T& operator[](std::size_t i) { return d_[i]; }
    const T& operator[](std::size_t i) const { return d_[i]; }
    std::size_t size() const noexcept { return n_; }
    std::size_t capacity() const noexcept { return cap_; }
};
Listing 3.64. A vector: growth and access. code/interviews-3/03-cpp/cpp/vec.hpp

new T[c] would need a default constructor, and would build cc objects, leaving a capacity with live objects in it. Here memory comes from std::allocator as raw storage, with objects created by placement new and destroyed by hand. The size n_ says exactly which slots hold objects, which is the invariant the class rests on.

The order gives both requirements. The new element goes first, while the old block is alive, so an argument that refers into the vector, v[0], is still valid. Our test does that 40 times with a 40-character string. And until the last steps, nothing visible has changed. If the new element’s constructor throws, or a copy during the transfer throws, the catch blocks undo what was done in the new block, and the vector is as it was. uninitialized_copy_n destroys its own partial work when it fails.

A throwing move would leave some old elements moved-from, so the transfer copies when it can, as the real vector does. A move-only type has no choice, and the test holds unique_ptrs.

To test the guarantee, use a type whose copy throws on a chosen count, at each position in turn, and compare the contents and capacity afterwards against a copy taken before. Then check the count of live objects, and run under AddressSanitizer. Here it runs 20 000 random steps against std::vector. A version made to move always fails it, as the test confirms.

Classic wrong answer. put(); d_ = fresh; before the transfer, or the new element built last. Either breaks v.emplace_back(v[0]) or the guarantee.

Question 3.57 ★★★★ QD, Dev, LLE • crypto, market maker, hedge fund • experienced

In C++20, implement a reference-counted pointer Rc<T>, a shared_ptr without weak pointers, with one allocation for count and object, and a count that is safe when several threads hold copies. Which memory order does the increment need? Which does the decrement need, and what breaks if it is relaxed? Show a test that would catch that on a strongly ordered x86-64. What does your class still not make safe?

Answer

Answer to Question 3.57.

The increment can be relaxed. The decrement needs acq_rel. Listing 3.65 is the class, with count and object in one block, and it matches shared_ptr on 20 000 random operations. It is clean under AddressSanitizer and ThreadSanitizer, with four threads racing to drop the last reference.

template <class T>
class Rc {
    struct Block {
        std::atomic<long> refs{1};
        T value;
    };
    Block* b_ = nullptr;
    explicit Rc(Block* b) : b_(b) {}

public:
    template <class... A>
    static Rc make(A&&... a) { return Rc(new Block{{1}, T(std::forward<A>(a)...)}); }
    Rc() = default;
    ~Rc() {
        if (b_ && b_->refs.fetch_sub(1, std::memory_order_acq_rel) == 1) delete b_;
    }
    Rc(const Rc& o) noexcept : b_(o.b_) {
        if (b_) b_->refs.fetch_add(1, std::memory_order_relaxed);
    }
    Rc(Rc&& o) noexcept : b_(std::exchange(o.b_, nullptr)) {}
    Rc& operator=(Rc o) noexcept {               // copy or move first, then swap and drop
        std::swap(b_, o.b_);
        return *this;
    }
    T& operator*() const { return b_->value; }
    T* operator->() const { return &b_->value; }
    long count() const { return b_ ? b_->refs.load(std::memory_order_relaxed) : 0; }
    explicit operator bool() const { return b_ != nullptr; }
};
Listing 3.65. A reference-counted pointer. code/interviews-3/03-cpp/cpp/rc.hpp

Incrementing is relaxed since a new reference can only be made from an existing one, which keeps the object alive throughout, and the increment need not order anything else.

The decrement has two jobs. A thread that used the object and drops its reference must have all its accesses finished before the count falls, which needs release. And the thread that reaches zero and deletes must see all those accesses as done, which needs acquire. If it is relaxed, another thread’s last read can be ordered after the delete, and the destructor may see stale members. A common refinement uses release on the decrement, with an acquire fence only when the count reaches zero, which avoids the acquire on every other decrement.

On x86-64, atomic read-modify-writes are full barriers, so a relaxed version normally runs fine, and a stress test will not fail on the hardware. That is why the test uses ThreadSanitizer, which reasons about the memory model and not the machine. It builds the class with the decrement changed to relaxed and reports a data race on the object’s data. The same driver with acq_rel reports none. So the test can fail the model. We did not run this on a weakly ordered machine.

It does not make one Rc object safe to change from two threads, as that is a data race on b_, and count() can be stale when it returns. Nor does it protect the T, whose members need their own synchronisation. It has no weak count, no custom deleter and no aliasing, and that is what makes it 8 bytes.

Classic wrong answer. “Every operation should be seq_cst, to be safe.” That is correct, and it costs on weakly ordered machines, and shows you did not work out what each one needs.

Question 3.58 ★★★★ QD, Dev, LLE • crypto, market maker, hedge fund • experienced

In C++20, implement Fn<R(A...)>: a move-only, type-erased callable with a 32-byte small buffer, falling back to the heap for a larger or a throwing-move callable. It must hold a lambda that captures a std::unique_ptr. How do you call a function whose type you have forgotten? What do you need beyond the call? Why must the buffer be used only for a callable with a noexcept move? Which allocations are left? What does std::function have that your class lacks?

Answer

Answer to Question 3.58.

Listings 3.66 and 3.67 are one implementation. You forget the type by writing functions while you still know it: Model<F, Small> generates a table of function pointers, one table per callable type, and Fn keeps a pointer to the table. Beyond the call, you need move_to and destroy.

template <class Sig> class Fn;

template <class R, class... A>
class Fn<R(A...)> {
    static constexpr std::size_t Cap = 32;
    struct Ops {
        R (*call)(void*, A&&...);
        void (*move_to)(void* from, void* to) noexcept;   // constructs at to, destroys from
        void (*destroy)(void*) noexcept;
    };
    alignas(std::max_align_t) unsigned char buf_[Cap];
    const Ops* ops_ = nullptr;

    template <class F, bool Small> struct Model {
        static F* get(void* p) {
            if constexpr (Small) return std::launder(static_cast<F*>(p));
            else return *static_cast<F**>(p);
        }
        static inline const Ops ops{
            [](void* p, A&&... a) -> R { return (*get(p))(std::forward<A>(a)...); },
            [](void* from, void* to) noexcept {
                if constexpr (Small) { ::new (to) F(std::move(*get(from))); get(from)->~F(); }
                else ::new (to) F*(get(from));
            },
            [](void* p) noexcept {
                if constexpr (Small) get(p)->~F();
                else delete get(p);
            }};
    };
Listing 3.66. A callable holder: the table for each type. code/interviews-3/03-cpp/cpp/fn.hpp
public:
    Fn() = default;
    template <class F, class D = std::decay_t<F>,
              class = std::enable_if_t<!std::is_same_v<D, Fn>>>
    Fn(F&& f) {
        constexpr bool small = sizeof(D) <= Cap && alignof(D) <= alignof(std::max_align_t)
                               && std::is_nothrow_move_constructible_v<D>;
        if constexpr (small) ::new (static_cast<void*>(buf_)) D(std::forward<F>(f));
        else ::new (static_cast<void*>(buf_)) D*(new D(std::forward<F>(f)));
        ops_ = &Model<D, small>::ops;
    }
    Fn(Fn&& o) noexcept : ops_(std::exchange(o.ops_, nullptr)) {
        if (ops_) ops_->move_to(o.buf_, buf_);
    }
    Fn& operator=(Fn o) noexcept {
        reset();
        if (o.ops_) { o.ops_->move_to(o.buf_, buf_); ops_ = std::exchange(o.ops_, nullptr); }
        return *this;
    }
    ~Fn() { reset(); }
    void reset() noexcept {
        if (ops_) ops_->destroy(buf_);
        ops_ = nullptr;
    }
    explicit operator bool() const noexcept { return ops_ != nullptr; }
    R operator()(A... a) { return ops_->call(buf_, std::forward<A>(a)...); }
};
Listing 3.67. A callable holder: the public side. code/interviews-3/03-cpp/cpp/fn.hpp

Inside the constructor, D is known, and the lambdas in Model<D, small> are written with it. After that, Fn sees only void* and three function pointers. This is type erasure, a virtual table written by hand.

The buffer holds a callable only when it is small enough, not over-aligned, and has a noexcept move. That last condition follows from Fn’s own noexcept move: a callable in the buffer must really move when Fn moves, whereas a heap callable needs only its pointer copied. If a move in the buffer could throw, Fn’s move could not honestly promise otherwise. The promise matters: a growing vector moves a type it cannot copy either way, but if such a move throws halfway, the elements already moved stay moved, and the vector’s strong guarantee is gone.

A lambda with four long captures is 32 bytes, and the test counts 0 allocations for it. One with an array of eight is 64 bytes, and costs 1. After being moved from, the source tests false, and the moved callable still works, without copying the 64 bytes. A lambda holding a unique_ptr works, as we never ask for a copy. Its differential test against std::function runs 20 000 random steps with a live-object count, which ends at 0, under AddressSanitizer.

std::function has copying, which is the reason it cannot hold a unique_ptr lambda. It also throws bad_function_call when empty, where calling our empty Fn is undefined, and it supports target. C++23 adds std::move_only_function, which is closest to this class.

Classic wrong answer. “Use a virtual base class and new for each callable.” That works, and it allocates each time. The hand-made table is what allows the small buffer.

Question 3.59 ★★★★ QD, Dev, LLE • any • experienced

In C++20, implement Var<Ts...>, a simplified std::variant, with emplace<T>, get_if<T>, index, visit and the destructor; copying may be deleted. How big must the storage be and how must it be aligned, for any list of types? How does the destructor know what to destroy? What state are you in if a constructor throws inside emplace? How do you find the index of a type at compile time, with no recursion?

Answer

Answer to Question 3.59.

Listing 3.68 is one implementation. The storage is the largest size among the types, aligned for the strictest of them, which alignas(Ts...) and std::max({sizeof(Ts)...}) state directly. The index says what is alive, and the destructor tests each type in a fold. If a constructor throws inside emplace, the variant holds nothing.

template <class... Ts>
class Var {
    static constexpr std::size_t none = sizeof...(Ts);
    alignas(Ts...) unsigned char buf_[std::max({sizeof(Ts)...})];
    std::size_t idx_ = none;

    template <class T> static constexpr std::size_t index_of = [] {
        static_assert((std::is_same_v<T, Ts> || ...), "T is not in the list");
        std::size_t i = 0, found = none;
        ((std::is_same_v<T, Ts> ? void(found = i) : void(), ++i), ...);
        return found;
    }();
    template <class T> T* as() { return std::launder(reinterpret_cast<T*>(buf_)); }
    void destroy() noexcept {
        std::size_t i = 0;
        ((idx_ == i++ ? as<Ts>()->~Ts() : void()), ...);
        idx_ = none;
    }
public:
    Var() = default;
    Var(const Var&) = delete;
    Var& operator=(const Var&) = delete;
    ~Var() { destroy(); }
    template <class T, class... A> T& emplace(A&&... a) {
        destroy();                               // if the constructor throws: valueless
        T* p = ::new (static_cast<void*>(buf_)) T(std::forward<A>(a)...);
        idx_ = index_of<T>;
        return *p;
    }
    bool valueless() const noexcept { return idx_ == none; }
    std::size_t index() const noexcept { return idx_; }
    template <class T>
    T* get_if() noexcept { return idx_ == index_of<T> ? as<T>() : nullptr; }
    template <class F> auto visit(F&& f) {
        std::common_type_t<decltype(f(std::declval<Ts&>()))...> out{};
        std::size_t i = 0;
        ((idx_ == i++ ? void(out = f(*as<Ts>())) : void()), ...);
        return out;
    }
};
Listing 3.68. A small variant. code/interviews-3/03-cpp/cpp/var.hpp

A pack in alignas applies each alignment and keeps the largest, so a 32-byte-aligned Wide forces the whole object to 32, which a static_assert in the test checks.

The destructor has to run the right destructor for a type known only at run time. The fold expands to one test per type: idx_ == 0 ? destroy first : nothing, then idx_ == 1 and so on, with i++ moving along the pack. visit works just the same. That is a linear chain of tests, which is fine for a few types. std::variant implementations typically generate a table or a switch instead.

emplace destroys the old value first, so that both never share the buffer, and sets idx_ to none. If the constructor then throws, idx_ stays none, and the destructor does nothing. That is the same state as valueless_by_exception. Without the destroy call, the old object would never be destroyed, and the test’s live count catches that.

index_of is an immediately invoked constexpr lambda with a fold over the pack: for each type, if it matches, record i, then step i. A comma fold evaluates left to right, so no recursion or helper template is needed. If a type appears twice in the list, the last match wins, where std::variant would refuse the call. A type not in the list must be refused at compile time, which the static_assert does. Without it, emplace<std::string> on a Var<int, char> compiles and overflows the buffer.

The test runs 30 000 random steps against std::variant with a monostate, covering a throwing emplace, and compares the index, the visit result and the number of live objects, under AddressSanitizer.

Classic wrong answer. “Use a union of Ts....” A union cannot be declared over a pack directly. It takes a recursive template, which is what the standard library does, and which also allows constexpr.

Question 3.60 ★★★★ QD, Dev, LLE, QR • any • experienced

d = a + b + c on vectors of 1000 doubles, with an ordinary operator+ returning a std::vector<double>, allocates twice and runs two addition loops. In C++20, write expression templates so that it allocates nothing, given that d already has its size, and runs one loop. What does a + b return now? Is a = a + b safe? Now auto e = a + b + c; use(e[0]);: what happens, and why did auto cause it? How would you repair the class?

Answer

Answer to Question 3.60.

a + b now returns a small Add object holding references to a and b, and does no arithmetic. The assignment to d runs one loop, asking the whole tree for each element, as in Listing 3.69. The test counts 0 allocations, where the plain version makes 2. a = a + b is safe here. auto e dangles, and AddressSanitizer reports stack-use-after-scope.

template <class E>
concept Expr = requires(const E& e, std::size_t i) {
    { e[i] } -> std::convertible_to<double>;
    { e.size() } -> std::convertible_to<std::size_t>;
};

template <Expr L, Expr R>
struct Add {
    const L& l;                                 // references: cheap, and the trap
    const R& r;
    double operator[](std::size_t i) const { return l[i] + r[i]; }
    std::size_t size() const { return l.size(); }
};

template <Expr L, Expr R>
Add<L, R> operator+(const L& l, const R& r) { return {l, r}; }

class Vector {
    std::vector<double> d_;

public:
    explicit Vector(std::size_t n, double v = 0) : d_(n, v) {}
    template <Expr E>
    Vector(const E& e) : d_(e.size()) { assign(e); }
    template <Expr E>
    Vector& operator=(const E& e) { assign(e); return *this; }
    double operator[](std::size_t i) const { return d_[i]; }
    double& operator[](std::size_t i) { return d_[i]; }
    std::size_t size() const { return d_.size(); }

private:
    template <class E>
    void assign(const E& e) {
        for (std::size_t i = 0; i < size(); ++i) d_[i] = e[i];   // one loop, no temporaries
    }
};
Listing 3.69. Expression templates. code/interviews-3/03-cpp/cpp/expr.hpp

The type of a + b + c is Add<Add<Vector, Vector>, Vector>, so the whole expression is in the type, and the compiler can inline e[i] down to a[i] + b[i] + c[i]. The Expr concept keeps operator+ from applying to arbitrary types.

a = a + b is safe, as each element depends only on the same index, so writing a[i] never affects a later read. An operation that reads other positions is not safe in this way: a matrix product or a shift reads elements that the loop has already written, which is why such libraries make a copy for those cases.

The trap is lifetime. a + b + c creates the inner Add as a temporary, and the outer one stores a reference to it. In d = a + b + c, the temporaries live until the end of the full expression, and that includes the loop. With auto e, the outer Add survives the line, and the inner one does not. Before, auto on a Vector result was harmless, which is why people write this. Nothing fails at compile time.

The repair is to store a named Vector by reference, and an inner expression by value, since an expression is only two references or a nested copy. Our test chooses the member type with conditional_t, and the auto version then runs clean under AddressSanitizer, giving 6. The named vectors must still outlive e.

Mismatched sizes are not checked here, as size looks only at the left operand. A real library asserts on them. We measured no timing.

Classic wrong answer. “The compiler would have optimised the temporaries away anyway.” The allocations in the plain version are real, and the test counts 2.