03 — Syntax Refresh Drills

When: W1–W4, alongside toolchain setup and the three projects. Format: 30 drills, 4 weekly batches. Each drill: Goal · Starter · Acceptance. Rule: Every drill compiles with clang++ -std=c++20 -Wall -Wextra -Wpedantic -Werror -O2 -g. If it doesn’t, you’re not done.

These drills are not “learn C++.” They are surgical strikes on the specific pieces of muscle memory two years of AI-assisted work has decayed. Each one is small enough to finish in 15–40 minutes. If a drill takes you more than an hour, stop, read the relevant cppreference page for the feature, and try again fresh.


Rules of engagement

  • No AI assistance. Copilot off. Cursor tab off. Claude closed. If you cannot type a for loop without autocomplete, that is exactly why you are doing these drills.

  • Hand-type every character — including the #includes. Do not copy-paste starters. Read them, close them, type them.

  • Compile with -Werror. Every warning is a bug. Fix it before moving on.

  • Read the compiler error out loud before you fix it. If you can’t restate what clang is saying, you don’t yet understand the bug.

  • Time-box each drill at 45 minutes. If you’re stuck past that, note the drill number, move on, come back Saturday.

  • Keep everything. One folder per drill: ~/cpp-drills/w1/01_reverse_string/. Commit them. You’ll want to reread them in Phase 2.


Batch W1 — Drills 1–8 · Reflex-level basics

The goal of W1 is to get I/O, strings, vectors, and control flow feeling ordinary again.

Drill 1 — hello with a twist

Goal: Prove the toolchain works end-to-end, use std::print from C++23.

Starter: blank file.

// Write a program that prints "Hello, {name}" where {name} comes from argv[1].
// If argv[1] is missing, print "Hello, world" instead.
// Use std::print or std::println (from <print>).

Acceptance: ./hello Raghul prints Hello, Raghul. ./hello prints Hello, world. No warnings on -Werror.


Drill 2 — reverse a string in place

Goal: Iterators, std::string mutability, avoiding an unnecessary copy.

Starter:

#include <string>
void reverse_in_place(std::string& s) {
    // your code here
}

Acceptance: reverse_in_place mutates s (no return value). Works on empty string, single char, odd length, even length. Do not use std::reverse — write the two-pointer loop yourself. Then write a second version that does use std::reverse and note in a comment which one you’d ship.


Drill 3 — FizzBuzz with std::string_view

Goal: string_view, avoiding allocation in a hot loop.

Starter: blank.

// Print 1..N. Multiples of 3 -> "Fizz". Multiples of 5 -> "Buzz".
// Multiples of both -> "FizzBuzz".
// Constraint: the "Fizz", "Buzz", "FizzBuzz" literals must be std::string_view constants,
// not std::string. No allocation in the loop.

Acceptance: Runs for N=100. Output matches classic FizzBuzz. Verify with a grep: ./fizzbuzz | grep -c FizzBuzz should print 6.


Drill 4 — word count on a vector

Goal: std::vector<std::string>, range-for, split by whitespace, std::istringstream.

Starter:

#include <string>
#include <vector>
std::vector<std::string> split_whitespace(const std::string& line) {
    // your code
}

Acceptance: split_whitespace("  the  quick  brown fox  ") returns {"the", "quick", "brown", "fox"}. Empty string returns empty vector. Single word returns 1-element vector.


Drill 5 — read a file line-by-line

Goal: std::ifstream, std::getline, error handling.

Starter: blank.

// Open the file passed as argv[1]. Print each line prefixed by "<line_num>: ".
// If the file cannot be opened, print an error to std::cerr and exit(1).
// Do not leak the ifstream; do not use manual close().

Acceptance: Test on a 100-line file and an empty file. ./readlines /nonexistent writes to stderr and exits nonzero. echo $? confirms.


Drill 6 — count and sort word frequencies

Goal: std::map (or std::unordered_map), sorting a vector<pair> by second element.

Starter:

#include <string>
#include <vector>
#include <utility>
// Read words from stdin. Print each unique word with its count,
// sorted by count descending, ties broken alphabetically.

Acceptance: echo "the cat sat on the mat the mat" | ./freq prints the 3, mat 2, then cat 1, on 1, sat 1 (each on its own line).


Drill 7 — command-line argument parser (tiny)

Goal: argc/argv, std::string_view, small state machine.

Starter:

// Support: --name X (or --name=X), --count N, --verbose (flag).
// Print the parsed values. Unknown flag -> error to stderr, exit(2).

Acceptance: ./args --name Raghul --count 3 --verbose prints all three. ./args --unknown exits with code 2.


Drill 8 — binary search on a sorted vector (hand-written)

Goal: Off-by-one discipline, size_t vs ptrdiff_t awareness, iterator arithmetic.

Starter:

#include <vector>
#include <optional>
std::optional<std::size_t> bsearch(const std::vector<int>& v, int target) {
    // your code — do NOT call std::binary_search or std::lower_bound
}

Acceptance: Passes 10 hand-written test cases: empty, single element hit, single element miss, target < min, target > max, duplicates. Compare against std::lower_bound in a second pass.


Batch W2 — Drills 9–16 · Modern C++ basics

This is where syntax turns into idiom. Every drill here has a “why does modern C++ do it this way” answer. Write that answer as a comment at the top of your file.

Drill 9 — structured bindings

Goal: Decompose std::pair, std::tuple, and structs.

// Given a std::map<std::string, int>, iterate with:
//   for (auto& [key, value] : m) { ... }
// Also decompose a struct:
//   struct Point { int x, y, z; };
//   auto [x, y, z] = get_point();
// Print the results.

Acceptance: Both loops compile and print correctly. Explain in a comment why auto& is preferred over auto in the map loop.


Drill 10 — lambdas with all four capture modes

Goal: Understand [=], [&], [x], [&x], [=, &x], [this].

// Write five lambdas, each demonstrating a different capture mode.
// Include one lambda that captures by move: [p = std::move(unique_ptr)]{ ... }.
// Show that a [&] capture of a local variable is a bug if the lambda outlives the scope.

Acceptance: All five lambdas execute. The bug demonstration is guarded by a // KNOWN UB comment and disabled by #if 0. Bonus: run the bug version under -fsanitize=address and see ASan catch it.


Drill 11 — std::optional for maybe-values

Goal: Stop using magic sentinels (-1, empty string) for “no result.”

std::optional<int> find_index(const std::vector<int>& v, int target);
// Return the index of target, or nullopt if absent.
// Then call it and use if (auto i = find_index(...); i) { ... }.

Acceptance: Works on hit, miss, empty vector. No sentinel values anywhere. Print the result using .value_or(-1) for the miss case.


Drill 12 — std::variant and std::visit

Goal: Sum types. Pattern matching. The “overloaded lambda” trick.

using Value = std::variant<int, double, std::string>;
// Write a function print(Value v) that uses std::visit with an overloaded lambda
// to print int/double/string differently.

Acceptance: Compiles with the overloaded-lambda idiom (struct overloaded : Ts... { using Ts::operator()...; };). Prints correctly for all three variant states. Explain in a comment why std::visit beats a chain of if (std::holds_alternative<T>(v)).


Drill 13 — rule of zero

Goal: The best special member functions are the ones you don’t write.

// Build a class Contact { std::string name; std::string email; std::vector<std::string> phones; };
// Do NOT write any of: destructor, copy ctor, copy assign, move ctor, move assign.
// Prove via a test that copy, move, and destruction all work correctly.

Acceptance: No user-defined special members. The class is copyable, movable, destructible correctly by default. A Contact in a std::vector<Contact> handles emplace/erase/resize without leaks (verify with ASan).


Drill 14 — rule of five

Goal: When you must manage a resource yourself.

// Build a class OwnedBuffer { char* data; std::size_t size; }; that owns a heap allocation.
// Implement all five: destructor, copy ctor, copy assign, move ctor, move assign.
// Move must leave the source in a valid empty state (data=nullptr, size=0).
// Copy must deep-copy the buffer.

Acceptance: Passes a test that copies, moves, self-assigns, and destructs. Run under ASan — zero leaks, zero double-frees. Then rewrite using std::unique_ptr<char[]> and note how much code disappears.


Drill 15 — constexpr factorial

Goal: Compile-time computation. static_assert.

constexpr std::uint64_t factorial(std::uint32_t n) {
    // your code
}
static_assert(factorial(0) == 1);
static_assert(factorial(5) == 120);
static_assert(factorial(10) == 3628800);

Acceptance: All three static_asserts pass. Attempt to compute factorial(21) and observe overflow — add a note about why.


Drill 16 — custom comparator with std::sort

Goal: Passing a lambda as a comparator. Sort by multiple keys.

struct Employee { std::string name; int salary; int hire_year; };
// Sort a std::vector<Employee> by salary descending, then hire_year ascending, then name ascending.
// Use a single lambda passed to std::sort.

Acceptance: Passes a hand-written test with 6 employees demonstrating each tiebreaker. No use of std::stable_sort (unless you argue in a comment why you’d prefer it).


Batch W3 — Drills 17–24 · Ownership, references, error handling

The hardest week. This is where the ex-Java brain gets confused. Slow down.

Drill 17 — std::unique_ptr basics

Goal: RAII replaces new/delete.

// Allocate a struct Node { int value; std::unique_ptr<Node> next; } as a linked list of 5 nodes.
// Traverse, print, and let the destructor clean up. No manual delete anywhere.

Acceptance: Compiles. ASan reports zero leaks. Print destructor invocations to confirm they run in the right order.


Drill 18 — std::shared_ptr and cycles

Goal: When shared ownership makes sense; the cycle trap.

// Build a Parent/Child graph where both sides hold shared_ptr to each other.
// Show the memory leak via ASan.
// Fix it by making one direction a std::weak_ptr.

Acceptance: Broken version leaks under ASan. Fixed version leaks zero bytes. Write a two-sentence comment explaining why weak_ptr breaks the cycle.


Drill 19 — pass by value vs const-ref vs rvalue-ref

Goal: When to take which. “Sink” arguments.

void take_by_value(std::string s);        // for sinks (constructor init, storage)
void take_by_const_ref(const std::string& s);  // for observers
void take_by_rvalue_ref(std::string&& s); // rare; usually take_by_value + move is better
// Write a function set_name(std::string name) that stores name in a member via std::move.

Acceptance: In a comment, explain why set_name(std::string name) + member_ = std::move(name) is preferred over set_name(const std::string& name). Compile and run.


Drill 20 — move semantics from scratch

Goal: Understand std::move is a cast, not an action.

// Write a class Buffer that owns a heap array (rule of five, see drill 14).
// Add a print statement inside move-ctor and copy-ctor.
// Then write: Buffer a{1000}; Buffer b = std::move(a); Buffer c = a;
// Explain in comments why b uses move-ctor and c uses copy-ctor after move.

Acceptance: Output shows one move-ctor call and one copy-ctor call. a after the move is valid but empty. c after copy is a full deep copy of the moved-from a.


Drill 21 — std::expected for error results (C++23)

Goal: Errors as values, not exceptions. Requires brew LLVM.

#include <expected>
std::expected<int, std::string> parse_int(std::string_view s);
// Return the parsed int, or an error message.
// Then chain two calls with .and_then() or manual if-checks.

Acceptance: Handles “42” → 42; “abc” → error; “” → error. Uses .value(), .error(), and .value_or(0). Compile with -std=c++23.


Drill 22 — exception basics (and their cost)

Goal: try/catch, throw, when NOT to use exceptions.

// Write a function that parses an int and throws std::invalid_argument on failure.
// Call it in a loop 1_000_000 times: half valid inputs, half invalid.
// Time it. Then rewrite using std::expected. Time that. Compare.

Acceptance: You measure and record both timings. Exception version is dramatically slower on the invalid half. Note the number in a comment.


Drill 23 — templates 101 — function template

Goal: Basic template syntax; the compile-error cliff.

template <typename T>
T max_of(T a, T b) { return a < b ? b : a; }
// Call with int, double, std::string. Then try max_of(1, 2.0) and read the error.
// Fix it two ways: explicit template arg, and an explicit cast.

Acceptance: Works for the three homogeneous cases. You’ve read and can explain the deduction-failure error in the mixed case.


Drill 24 — templates 102 — class template

Goal: A minimal Stack<T> on top of std::vector<T>.

template <typename T>
class Stack {
    std::vector<T> data_;
public:
    void push(T value);
    T pop();                 // throws or returns optional on empty — your choice, document it
    bool empty() const noexcept;
    std::size_t size() const noexcept;
};

Acceptance: Instantiate Stack<int>, Stack<std::string>, and Stack<std::unique_ptr<int>>. The last one forces you to think about move-only types. Push, pop, size correctly for all three.


Batch W4 — Drills 25–30 · Streams, regex, signals

The last batch bridges into the P0.3 log-tail project. Every drill here reappears there.

Drill 25 — std::stringstream for building strings

Goal: Build a formatted string without sprintf.

#include <sstream>
// Given a vector of ints, produce a string like "[1, 2, 3, 4]" using std::ostringstream.
// Then rewrite using std::format from <format>. Compare readability.

Acceptance: Both versions produce identical output. Note in a comment which you’d ship in 2026 and why.


Drill 26 — std::regex — match, search, replace

Goal: Regex API surface. It’s clunky. Learn it once.

#include <regex>
// Given a log line "[2026-07-15 14:23:01] INFO auth: user=raghul action=login",
// extract timestamp, level, subsystem, and the key=value pairs.
// Use std::regex + std::smatch.

Acceptance: All four fields extracted correctly. Handles a malformed line by printing an error, not by crashing.


Drill 27 — std::filesystem — walk a directory

Goal: No more opendir/readdir C calls.

#include <filesystem>
// Print all .cpp files under a given root directory, recursively, with their file size.
// Skip hidden dirs (starting with '.'), skip symlinks.

Acceptance: Works on your ~/cpp-drills folder. Output sorted by size descending.


Drill 28 — std::chrono — timing a block

Goal: Modern time API. You’ll use this all year.

#include <chrono>
// Time how long it takes to insert 10,000,000 ints into a std::vector<int> with:
//   (a) push_back in a loop
//   (b) push_back after reserve(10_000_000)
//   (c) direct construction with v(10_000_000, 0)
// Report all three timings in microseconds.

Acceptance: All three timings printed. reserve should be dramatically faster than naive push_back. Note the ratio in a comment.


Drill 29 — signal handling with std::signal

Goal: Catch Ctrl-C cleanly. Prep for the log-tail project.

#include <csignal>
#include <atomic>
static std::atomic<bool> keep_running{true};
void on_sigint(int) { keep_running.store(false); }
// In main: register handler, then loop until keep_running is false.
// Print "exiting cleanly" on the way out.

Acceptance: Ctrl-C stops the loop and prints “exiting cleanly” before exit. No double Ctrl-C to kill.


Drill 30 — std::regex performance sanity check

Goal: Understand that std::regex is slow. Really slow.

// Compile a regex once (as a static const std::regex).
// Match it against 1_000_000 log lines.
// Time it. Then compile the regex inside the loop and time that. Compare.

Acceptance: Static compilation is at least 100x faster. You’ve verified this on your laptop and noted the numbers. This is why in P0.3 you compile the regex once, outside the loop.


What to do after all 30 pass

Delete none of them. Review them together on Sunday of W4:

  1. Skim the 30 folders.

  2. For each drill, ask: “if the study partner asked me this at 11 PM tomorrow, could I redo it in 20 minutes without hesitation?” Star the ones where the answer is no.

  3. Re-do the starred drills fresh.

The drills you struggle with the second time are your Phase 0 weak spots. Fix them before Phase 1.


What most people get wrong

They do the drills too fast, don’t compile with -Werror, and never rerun the ones they got wrong. Six weeks later they hit std::variant in real code and stall. The rerun on Sunday of W4 is not optional — it’s the phase’s most valuable single hour.


Return to Phase 0 README · Next: 04_first_projects.md