02_Modern_CPP — All exercises¶
Every exercise from the "Try it" section of all 14 modules, in one place. The module file has the full wording and the exact "done when" condition; this list gives the short version plus a hint.
Build everything in WSL with g++ -std=c++17 -Wall -Wextra -Werror -pthread unless the exercise
says otherwise. Do the easy ones the same day you read the module. Do at least one medium per
module. Pick the hard ones that match the job you want: the ownership, invalidation, vtable and
performance ones come up most in interviews.
Difficulty count: 28 easy, 29 medium, 24 hard (81 total).
01. From C to C++ (01_from_c_to_cpp.md)¶
- (easy)
swap_values(int&, int&)with references, plus the C pointer version.- Hint: a reference must bind to an object at initialisation, so there is no
nullptrto pass. Tryswap_values(*p, *q)with a nullpand note that the UB moves to the caller's dereference.
- Hint: a reference must bind to an object at initialisation, so there is no
- (easy) Build
m01_extern_c_mangling.cpp, runnmwith and without-C, and decode the mangled name ofstd::string greet(const std::string&, int).- Hint:
R= reference,K= const,P= pointer,i= int,c= char.std::stringshows up as a longNSt7__cxx1112basic_string...chunk;c++filtconfirms your reading.
- Hint:
- (medium) Rename a 100-line C file to
.cppand fix every error under-Werror.- Hint:
mallocneedsstatic_cast<T*>, identifiers namednew/class/thismust be renamed, and string literals areconst char*, notchar*.
- Hint:
- (medium) C header +
gcc -cobject, called from C++ withextern "C"guards.- Hint: wrap the declarations in
#ifdef __cplusplus extern "C" { #endifin the header itself, not inmain.cpp, so every C++ includer gets it for free.
- Hint: wrap the declarations in
- (hard) Predict which
logoverload is chosen forNULL,0,'x',3.0f,nullptr.- Hint: GCC defines
NULLas__null, an integer zero with the width of a pointer, so on 64-bit Linuxlog(NULL)pickslong, notconst char*(other compilers differ).3.0fis a float-to-double promotion;'x'promotes toint; onlynullptrreachesconst char*.
- Hint: GCC defines
02. Classes, constructors and destructors (02_classes_constructors_destructors.md)¶
- (easy) Predict the output of
m02_ctor_dtor_order.cppon paper, then run it.- Hint: bases first, then members in declaration order, then the constructor body. Destruction is the exact reverse.
- (easy) Add
Tracer ctoDerivedand list it first in the initialiser list; see-Wreorder.- Hint: members are initialised in declaration order, never in initialiser-list order.
- (medium)
Temperaturewith anexplicitconstructor and afrom_fahrenheitfactory.- Hint:
explicitblocks copy-initialisation (T t = 36.6;) but not direct-initialisation (T t{36.6};). A named static factory documents the unit at the call site.
- Hint:
- (medium) A class whose second member's constructor throws; observe which destructors run.
- Hint: only fully constructed subobjects are destroyed. The outer object's lifetime never began, so its destructor does not run. This is why each resource needs its own RAII member.
- (hard) Function-local static singleton; find
__cxa_guard_acquirein-O2 -Soutput.- Hint: look for a byte-sized guard variable tested with a plain load before the call. Once it is set, later calls take the fast path and never reach the lock.
03. RAII (03_raii_the_core_idea.md)¶
- (easy) Add an early-
returnpath tom03_file_handle_raii.cpp; showfclosestill runs.- Hint: put a
printfin the destructor. Every path that leaves the scope runs it.
- Hint: put a
- (easy)
DirHandlewrappingopendir/closedir, move-only, listing/tmp.- Hint: copy
FileHandle's shape exactly: delete copy, write move ctor/assign that null the source, and check for null in the destructor.
- Hint: copy
- (medium) Rewrite the C
process()from section 2.1 with RAII; count lines before and after.- Hint: each
goto cleanuplabel becomes one RAII local. Declare them in acquisition order so the reverse-order destruction matches what the C code did by hand.
- Hint: each
- (medium) Generic
ScopeGuardwithdismiss(), used to roll backmap.erase(key).- Hint:
template <class F> class ScopeGuard { F f_; bool active_ = true; ... }. Make the destructornoexceptand callf_()only whileactive_. Calldismiss()after the last step succeeds.
- Hint:
- (hard)
strace -e trace=socket,bind,listen,closeonm03_socket_fd_raii.cpp, then add a double-close bug and seeEBADF.- Hint: the second
closeon the same number fails because the kernel already freed that fd slot. In a threaded server the slot may have been reused, so the "harmless" bug closes someone else's socket.
- Hint: the second
- (hard) Find the landing pad for
write_reportin-O2 -Soutput.- Hint: search for
_Unwind_Resume. The block just before it callsfclose; the normal path reachesfcloseseparately and has no flag checks. The.gcc_except_tablemaps call sites to pads.
- Hint: search for
04. Value semantics, copy and move (04_value_semantics_copy_move.md)¶
- (easy) Build
m04_copy_elision_nrvo.cppwith and without-fno-elide-constructors.- Hint: in C++17, prvalue elision is mandatory and the flag cannot disable it; only NRVO and the move from a named local appear when you pass the flag.
- (easy) Drop
noexceptfromUniqueFd's move constructor andstatic_assertit.- Hint:
vectorusesstd::move_if_noexceptduring reallocation. A move-only type is still moved (there is no copy to fall back on), but the strong guarantee is lost.
- Hint:
- (medium)
Matrixwith rule of five and copy-and-swap; zero copies inMatrix m = a * b;.- Hint:
operator*returns a prvalue, somis constructed in place. WriteMatrix& operator=(Matrix other) noexcept { swap(other); return *this; }to get both assignments at once.
- Hint:
- (medium) Sink
set_name(std::string)vs theconst&/&&overload pair; 6-cell table.- Hint: the sink version costs one extra move in every case. The pair wins only when moves are
expensive or the function is very hot; a
std::stringmove is a few pointer stores.
- Hint: the sink version costs one extra move in every case. The pair wins only when moves are
expensive or the function is very hot; a
- (hard) Fixed-capacity container with
template <class... Args> T& emplace(Args&&...).- Hint:
::new (static_cast<void*>(slot)) T(std::forward<Args>(args)...). Withoutstd::forwardevery argument is an lvalue and you get a COPY where you expected a MOVE.
- Hint:
- (hard) Fix
void f(std::string&& s) { names.push_back(s); }.- Hint:
shas a name, so it is an lvalue andpush_back(const T&)is chosen. Usestd::move(s). The compiler is right not to warn: the code is valid, just slower.
- Hint:
05. Smart pointers and ownership (05_smart_pointers_and_ownership.md)¶
- (easy)
unique_ptralias forDIR*with a stateless deleter;static_assertonsizeof.- Hint:
struct DirCloser { void operator()(DIR* d) const noexcept { closedir(d); } };. An empty deleter takes no space thanks to the empty-base optimisation. A function pointer deleter doubles the size.
- Hint:
- (easy) Make
Node::parentashared_ptrand run undervalgrind --leak-check=full.- Hint: the report shows "definitely lost" for the root block and "indirectly lost" for the
children. The cycle keeps every strong count at 1 or more after
main's pointer dies.
- Hint: the report shows "definitely lost" for the root block and "indirectly lost" for the
children. The cycle keeps every strong count at 1 or more after
- (medium)
Connection : enable_shared_from_thiswith an async callback that keeps it alive.- Hint: capture
self = shared_from_this()in the lambda. The object must already be owned by ashared_ptr(create it withmake_shared), orshared_from_thisthrowsbad_weak_ptr.
- Hint: capture
- (medium) Toy
SharedPtr<T>with a plain then an atomic count; benchmark 1 vs 4 threads.- Hint: the atomic increment is a
lock xadd. From four threads the cache line holding the count bounces between cores, so it is much slower per copy than the single-thread case.
- Hint: the atomic increment is a
- (hard) Ownership graph for a tcpkv-style server, with a type on every edge.
- Hint:
Serverowns the listener by value, the pool by value, and the store by value. Workers receive the store by reference. A connection is aunique_ptrmoved into the task.shared_ptronly appears if a connection truly has two owners (for example, async I/O plus a registry).
- Hint:
06. STL containers (06_stl_containers_deep_dive.md)¶
- (easy) Element type with a non-
noexceptmove inm06_vector_growth.cpp; see copies.- Hint: the rule is "move if nothrow-movable or not copyable, else copy", so that a throw half-way through reallocation leaves the old buffer intact.
- (easy) 1M ints in
std::setvs a sortedvector; time inserts and lookups.- Hint: each set node is a separate allocation of roughly 40 bytes (typical on 64-bit libstdc++), so every step of a lookup is a likely cache miss. Binary search on a vector touches far fewer lines.
- (medium)
AccountKey { branch, number }as a key forunordered_mapandmap.- Hint: specialise
std::hash<AccountKey>in namespacestdand combine withh1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2)). Formap,std::tie(a.branch, a.number) < std::tie(...).
- Hint: specialise
- (medium) LRU cache with
std::listplusunordered_map<K, list::iterator>.- Hint:
list::splicerelinks the node without copying or destroying it, so every iterator to it stays valid. Evict fromback()and erase the map entry using the key stored in the node.
- Hint:
- (hard) Three deliberate invalidation bugs under
-D_GLIBCXX_DEBUGand then ASan.- Hint: the debug STL reports "attempt to dereference a singular iterator" at the bad line. ASan only reports if freed memory is actually touched, so the vector case is the one it catches best.
- (hard)
std::mapvs sortedvector<pair>+lower_boundat 1k, 100k, 10M.- Hint: at 1k both fit in L1/L2 and differ little. As size grows the map's pointer chasing costs a cache miss per level while the vector's top levels stay hot. Report what you measure, not what you expect.
07. STL algorithms, lambdas, iterators (07_stl_algorithms_lambdas_iterators.md)¶
- (easy) Replace sum, find-first and remove-matching loops with algorithms.
- Hint:
std::accumulateneeds<numeric>and an initial value of the right type (0LL, not0, for paise). Remove-matching isv.erase(std::remove_if(...), v.end()).
- Hint:
- (easy)
<=comparator on 1000 equal elements, under-D_GLIBCXX_DEBUGor ASan.- Hint: strict weak ordering requires
comp(a, a) == false. With<=the unguarded inner loop of introsort can run past the end of the array.
- Hint: strict weak ordering requires
- (medium)
top_kwithsort,partial_sort,nth_element+ sort of the prefix.- Hint: full sort is O(n log n);
partial_sortis O(n log k) with a heap;nth_elementis O(n) average and then sorting k items is trivial. Expectnth_elementto win for k = 10.
- Hint: full sort is O(n log n);
- (medium)
vector<std::function>handler registry vs astd::tupleof lambdas.- Hint:
std::functionis an indirect call that the optimiser usually cannot inline. The tuple version, walked withstd::applyand a fold, gives the compiler every concrete type to inline.
- Hint:
- (hard) Zero-copy word-frequency counter with
string_viewkeys andpartial_sort.- Hint: keep the whole file in one
std::stringthat outlives the map, or every key dangles. Callreserveon the map to avoid rehash churn.
- Hint: keep the whole file in one
- (hard) Bidirectional-only algorithm that rejects forward iterators via
iterator_traits.- Hint:
static_assert(std::is_base_of_v<std::bidirectional_iterator_tag, typename std::iterator_traits<It>::iterator_category>, "needs bidirectional"). Random-access tags derive from bidirectional, sovectorpasses.
- Hint:
08. Templates (08_templates_and_generic_programming.md)¶
- (easy)
print_allwith a type that has nooperator<<; find "required from here".- Hint: read the error bottom-up. The first "required from here" line that names your own file is the call to fix.
- (easy)
static_assert(std::is_arithmetic_v<T>, ...)at the top ofmax_of.- Hint: the assertion fires before the body is instantiated, so the long cascade disappears.
- (medium)
StaticVector<T, N>with placement new into aligned storage.- Hint:
alignas(T) std::byte buf_[N * sizeof(T)];, construct with placement new, destroy withstd::launder(reinterpret_cast<T*>(...))->~T()only for indices belowsize_.
- Hint:
- (medium)
has_size_vwithstd::void_t, then the same check withif constexpr.- Hint: primary template
: std::false_type; specialisation onstd::void_t<decltype(std::declval<T&>().size())>: std::true_type.
- Hint: primary template
- (hard) Variadic
compose(fs...)applying right-to-left;compose()is identity.- Hint: recurse:
compose(f, rest...)returns[=](auto x) { return f(compose(rest...)(x)); }. The base case is a lambda that returns its argument unchanged.
- Hint: recurse:
- (hard)
-ftime-reporton a heavy file, thenextern templateacross five.cppfiles.- Hint: put
extern template class Foo<int>;in the header and onetemplate class Foo<int>;in exactly one.cpp. Measure total wall time with a clean build each time.
- Hint: put
09. OOP, inheritance, virtual (09_oop_inheritance_virtual.md)¶
- (easy)
-fdump-lang-classonm09_vtable_demo.cpp; readVtable for Circle.- Hint: slot 0 is offset-to-top, slot 1 is the typeinfo pointer, then the function entries. The two destructor entries are the "complete object" destructor and the "deleting" destructor.
- (easy) Remove
virtualfrom~Shape; find-Wdelete-non-virtual-dtor.- Hint:
delete base_ptrthen calls only~Shape, so the derived part is never destroyed. It is UB formally, and in practice it leaks whatever the derived class owns.
- Hint:
- (medium) C
struct ops+void* ctxrewritten as an abstractDeviceinterface.- Hint: the function pointer table becomes the vtable and
ctxbecomes the derived class's members. Tests inject aFakeDevicethrough aDevice¶meter.
- Hint: the function pointer table becomes the vtable and
- (medium) 100M
shape->area()calls: uniform, alternating, andstd::variant+std::visit.- Hint: uniform types let the indirect-branch predictor hit every time. Alternating may still
predict well on modern CPUs; measure.
variantkeeps objects contiguous and lets the compiler inline.
- Hint: uniform types let the indirect-branch predictor hit every time. Alternating may still
predict well on modern CPUs; measure.
- (hard) Plugin
.sowithextern "C" Plugin* create(), loaded bydlopen/dlsym.- Hint:
dlsymlooks up a plain string, so the factory must not be mangled. Virtual calls need no symbol lookup at all: they go through the vptr stored in the object. Link the host with-ldlon older glibc.
- Hint:
- (hard)
-O2 -Son calls through afinalclass vs a non-final one.- Hint: with
final, the compiler knows the dynamic type exactly and emits a directcall(or inlines it). Without it you see a load of the vptr thencall QWORD PTR [rax+N].
- Hint: with
10. Error handling (10_error_handling_exceptions_optional_expected.md)¶
- (easy) Swap the two lines in
add_strongand throw on the 3rd allocation.- Hint: if the total is updated before the push that can throw, a failed push leaves the total wrong. Do the throwing step first, then the non-throwing commit.
- (easy)
std::optional<Account> find_account(std::string_view)with no sentinel values.- Hint:
if (auto a = find_account("X")) { use(a->balance); }.value_orneeds a full defaultAccount, so it suits small types best.
- Hint:
- (medium)
KvErrenum,KvCategory,make_error_code, andis_error_code_enum.- Hint: specialise
std::is_error_code_enum<KvErr> : std::true_typein namespacestd, and putmake_error_codein the same namespace asKvErrso argument-dependent lookup finds it.
- Hint: specialise
- (medium)
Expected<T, E>onstd::variant<T, E>with a short-circuitingmap.- Hint:
map(f)returnsExpected<decltype(f(value())), E>; ifthisholds an error, return that error without callingf. Make sureTandEare distinct types, or the variant is ambiguous.
- Hint:
- (hard)
optional<int>vs throwingparse_portat 0%, 1%, 50% invalid input.- Hint: the throwing version is free on the happy path and costs on the order of a microsecond per throw (typical), because unwinding walks tables and allocates the exception object.
- (hard) Thread-pool worker that captures task exceptions into
std::exception_ptr.- Hint:
catch (...) { result.ex = std::current_exception(); }in the worker, andif (ex) std::rethrow_exception(ex);inget(). The original dynamic type and its members survive.
- Hint:
11. const and constexpr (11_const_constexpr_and_compile_time.md)¶
- (easy) Find
kCrcTablein.rodataand0xcbf43926inmain.- Hint: search the
.sfile for.section .rodata. GCC prints 32-bit immediates as signed decimals, so0xcbf43926appears as$-873187034, not in hex.
- Hint: search the
- (easy)
constexpr days_in_monthwithstatic_assertper month, then an out-of-bounds read.- Hint: UB is forbidden in constant evaluation, so the compiler must reject it; the message says something like "array subscript value is outside the bounds".
- (medium)
constexpr std::array<std::string_view, 6>enum-to-string table.- Hint: add a
CountorLastenumerator andstatic_assert(table.size() == size_t(E::Count)). Adding a value without a name then breaks the build.
- Hint: add a
- (medium)
constexprsieve producing 1000 primes; time the compile, then push to 100,000.- Hint: GCC caps constexpr loops (
-fconstexpr-loop-limit, default 262144) and total operations (-fconstexpr-ops-limit). Compile-time work moves cost from every run to every build.
- Hint: GCC caps constexpr loops (
- (hard)
constexprstate-machine table andconstexpr bool accepts(std::string_view).- Hint:
constexpr State table[5][4] = {...};indexed by[state][input_class(c)]. The same function used at runtime reads the same.rodatatable.
- Hint:
12. Memory layout and performance (12_memory_model_layout_and_performance.md)¶
- (easy) Reorder
Badby hand inm12_padding_alignment.cpp; predict offsets; addstatic_assert.- Hint: sort members from largest alignment to smallest. Size is rounded up to the largest alignment.
- (easy) Change padding to 32 bytes in
m12_false_sharing_bench.cpp.- Hint: two counters 32 bytes apart can still share a 64-byte line, so the slowdown returns. Note that adjacent-line prefetch on some Intel CPUs can make 64 behave worse than 128.
- (medium)
Arena::destroy_all()with a list of finalisers for non-trivial objects.- Hint: store
{ptr, [](void* p) { static_cast<T*>(p)->~T(); }}when!std::is_trivially_destructible_v<T>. Run finalisers in reverse order of creation.
- Hint: store
- (medium)
m12_bench_basics.cppat-O0,-O2,-O3 -march=native, and underperf stat.- Hint:
perf stat -e cache-misses,instructions,cycles ./bench. The list's cache-miss count explains its time much better than its instruction count does.
- Hint:
- (medium)
SmallVector<T, N>with inline storage and heap spill.- Hint: a move of the inline case must move each element (the buffer lives inside the object). A move of the heap case steals the pointer and resets the source to inline mode.
- (hard) Signed overflow check, null check after dereference, float read through
int*.- Hint: the optimiser assumes UB never happens:
x + 1 < xbecomesfalse, the null check is deleted because the earlier dereference "proved" non-null, and strict aliasing lets it reorder the load.-fsanitize=undefinedreports the first;-fsanitize=nullthe second.
- Hint: the optimiser assumes UB never happens:
- (hard)
std::pmr::unsynchronized_pool_resourcefor anunordered_map<int, std::string>.- Hint: use
std::pmr::unordered_map<int, std::pmr::string>so the strings use the pool too. Check peak RSS with/usr/bin/time -v: pools often trade lower time for higher peak memory.
- Hint: use
13. C++17 features tour (13_cpp17_features_tour.md)¶
- (easy) Remove
[[maybe_unused]]and build with-DNDEBUG; drop thetry_reservecheck.- Hint: under
-DNDEBUGtheassertvanishes, the variable becomes unused, and-Werrorfails. Ignoring a[[nodiscard]]result triggers-Wunused-result.
- Hint: under
- (easy)
const std::string&tostd::string_view; count allocations with a countingoperator new.- Hint: replace global
operator new(std::size_t)to increment a counter. Literals longer than the SSO buffer (15 chars in libstdc++) allocate when passed asconst std::string&.
- Hint: replace global
- (medium) Crash-safe
config_reloadvia.tmp+fsync+fs::rename.- Hint:
renameis atomic on the same filesystem.fs::has nofsync, so open withopen(), write,fsync(fd), close, then rename. For full durability alsofsyncthe directory.
- Hint:
- (medium)
shared_mutexvsmutexacross reader:writer ratios and critical-section lengths.- Hint: with a 100 ns critical section the shared lock's extra atomic work often loses. With a 10 us critical section and many readers, parallel reading wins. See 04_Concurrency module 04.
- (hard)
std::sortvsstd::sort(std::execution::par, ...)with TBB at 1M, 10M, 100M.- Hint: link with
-ltbb. At 1M the thread start-up and merge overhead eats most of the gain.
- Hint: link with
- (hard)
TypeMap<Ts...>onstd::tuple<std::optional<Ts>...>with fold-basedvisit_all.- Hint:
std::get<std::optional<T>>(tuple_)selects by type, so allTsmust be distinct.visit_all:std::apply([&](auto&... o) { ((o ? f(*o) : void()), ...); }, tuple_);.
- Hint:
14. Writing production C++ (14_writing_production_cpp.md)¶
- (easy) Split
m14_library_api_demo.cppintokv_store.cpp+main.cpp; build with CMake; run clang-tidy.- Hint: set
CMAKE_EXPORT_COMPILE_COMMANDS=ONsoclang-tidy -p buildcan find the flags.
- Hint: set
- (easy) Two different
struct Configdefinitions in two.cppfiles.- Hint: this is an ODR violation, "ill-formed, no diagnostic required". The linker never sees types,
so it links silently.
-flto -Wodrcompares type layouts at link time and catches it.
- Hint: this is an ODR violation, "ill-formed, no diagnostic required". The linker never sees types,
so it links silently.
- (medium)
SocketFdheader in the tcpkv layout, test via public header,-fvisibility=hidden.- Hint:
#define TCPKV_API __attribute__((visibility("default")))on exported classes and functions only. Thennm -D --defined-only libtcpkv.so | c++filtshould be short.
- Hint:
- (medium) Run clang-tidy (
bugprone-*,performance-*,modernize-*,readability-*) on a 200-line file.- Hint: expect
modernize-use-auto,performance-unnecessary-value-param, andreadability-magic-numbersto dominate. Fix the cause, never addNOLINT.
- Hint: expect
- (hard) Public header for tcpkv's move-only
Connectionwith documented contracts.- Hint: delete copy,
[[nodiscard]]on every result, returnstd::optional<Message>plus anstd::error_code&out-parameter or a small result struct. State in a comment which methods are safe to call from two threads (likely none).
- Hint: delete copy,
- (hard) CI pipeline: clang-format,
-Werror, ASan+UBSan, TSan, clang-tidy on the diff.- Hint: TSan and ASan cannot be combined in one binary; build twice.
bugprone-use-after-movecatches the use-after-move statically; TSan catches the data race at run time.
- Hint: TSan and ASan cannot be combined in one binary; build twice.