← Back to all projects

LEARN ADVANCED CPP DEEP DIVE

Learn Advanced C++: From Concurrency to Coroutines

Goal: Go beyond the fundamentals of C++ to master the advanced features that define modern, high-performance systems programming: exception safety, multithreading, template metaprogramming, and C++20 coroutines.


Why Learn Advanced C++?

Writing basic C++ is one thing; writing robust, efficient, and concurrent C++ is another. The topics you’ve chosen are the pillars of modern C++ development. Understanding them allows you to:

  • Build Resilient Systems: Write code that is safe in the face of exceptions and errors, preventing leaks and corruption.
  • Leverage Modern Hardware: Write concurrent code that takes full advantage of multi-core processors.
  • Write More Expressive and Safer Code: Use template metaprogramming to enforce invariants and perform computations at compile time, catching errors before the program even runs.
  • Simplify Asynchronous Code: Use C++20 coroutines to write non-blocking code that looks as simple as synchronous code, eliminating “callback hell”.

After completing these projects, you’ll be equipped to tackle performance-critical applications, from game engines and trading systems to high-throughput servers.


Core Concept Analysis

1. Exception Handling & RAII

The core C++ philosophy for error handling is RAII (Resource Acquisition Is Initialization). It means you tie the lifetime of a resource (memory, file handle, lock) to the lifetime of an object. When the object goes out of scope, its destructor automatically cleans up the resource. This makes code safe even when exceptions are thrown.

void process_file(const std::string& filename) {
    std::ifstream file(filename); // RAII: file is acquired
    if (!file.is_open()) {
        throw std::runtime_error("Failed to open file");
    }
    
    // ... do work with file ...
    
} // Exception thrown or function returns: `file` destructor is called,
  // automatically closing the file. No leaks!

2. Concurrency Primitives

Primitive Purpose Analogy
std::thread The fundamental unit of execution. A worker you can hire to do a job.
std::mutex A mutual exclusion lock to protect shared data. A single-key lock for a room. Only one person can enter at a time.
std::condition_variable A mechanism to wait for a condition to become true. A doorbell. You wait (sleep) until someone rings it.
std::atomic<T> A type that guarantees atomic (uninterruptible) operations. A special counter that can’t be misread or miswritten by two people at once.
std::async / std::future A high-level way to run a function asynchronously and get its result later. Ordering a pizza. You get a receipt (future) and can do other things until the pizza (result) is ready.

3. Template Metaprogramming (TMP)

TMP is “programming at compile time.” The compiler executes your template code to generate types or constants.

  • Primary Use: Create generic, type-safe, and highly optimized code.
  • Example: A type trait is_pointer<T> that resolves to true if T is a pointer, and false otherwise, all before main() is ever run.

4. C++20 Coroutines

Coroutines are “resumable functions.” They allow you to write asynchronous, non-blocking code that looks simple and sequential.

// This looks synchronous, but is fully asynchronous!
task<void> read_data() {
    auto socket = co_await open_socket("server.com", 1234);
    auto data = co_await socket.read(); // Suspends here, doesn't block thread
    process(data);
    co_return;
}

The co_await keyword is the magic: it suspends the function without blocking the thread, and resumes it when the awaited operation (like a network read) is complete.


Project List


Project 1: The Exception-Safe Vector

  • File: LEARN_ADVANCED_CPP_DEEP_DIVE.md
  • Main Programming Language: C++
  • Alternative Programming Languages: Rust (for comparison of error handling)
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Exception Safety / RAII
  • Software or Tool: C++17 compiler, Valgrind or ASan
  • Main Book: “Effective C++, 3rd Edition” by Scott Meyers

What you’ll build: A simplified version of std::vector (a dynamic array) with a fanatical focus on providing the strong exception guarantee for its push_back method.

Why it teaches exception handling: This project forces you to confront std::bad_alloc. When your vector needs to resize, it allocates new memory. If that allocation fails, you must ensure your vector is left in its original, valid state, with no memory leaks. This is the heart of robust C++ error handling.

Core challenges you’ll face:

  • Implementing the copy-and-swap idiom → maps to the classic technique for achieving the strong exception guarantee
  • Using RAII for temporary allocations → maps to wrapping new memory in a std::unique_ptr until you can safely commit to it
  • Writing noexcept correct code → maps to marking functions like destructors and swap as noexcept
  • Distinguishing between the basic and strong exception guarantees → maps to understanding different levels of safety

Key Concepts:

  • Exception Guarantees: Basic, Strong, and Nothrow. (Scott Meyers explains this well).
  • RAII (Resource Acquisition Is Initialization): “Effective C++” Item 13.
  • Copy-and-Swap Idiom: A common C++ pattern for assignment and exception safety.

Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Strong C++ fundamentals, memory management (new/delete), templates.

Real world outcome: You’ll have a custom Vector<T> class that you can test with a helper that forces allocations to fail.

// Your vector should survive this test without leaking memory
// or becoming corrupted.
Vector<MyType> v;
try {
    // Force allocations to fail after a certain point
    set_allocation_failure_countdown(100);
    for (int i = 0; i < 1000; ++i) {
        v.push_back(MyType(i)); // This will eventually throw bad_alloc
    }
} catch (const std::bad_alloc& e) {
    std::cout << "Caught expected exception.\n";
    // CRUCIAL TEST: The vector should still be valid and contain
    // the elements that were successfully added before the failure.
    assert(v.size() < 1000 && v.capacity() > 0);
    std::cout << "Vector is in a valid state!\n";
}
// When 'v' goes out of scope, its destructor should free all memory correctly.
// Valgrind or ASan should report zero leaks.

Implementation Hints: The key is the resize logic in push_back.

  1. When capacity is reached, allocate a new, larger block of memory. Do not immediately assign it to your internal data pointer. Put it in a std::unique_ptr to ensure it’s freed if an exception occurs.
  2. Copy/move the elements from the old block to the new block. An element’s copy/move constructor could throw! If it does, the unique_ptr destructor will fire, freeing the new memory, and your vector’s old data remains untouched (strong guarantee).
  3. After all elements are safely copied, you can then release the pointer from the unique_ptr and delete[] the old memory block.

Learning milestones:

  1. Your Vector works for basic cases → You have a working dynamic array.
  2. Your push_back doesn’t leak memory if a copy constructor throws → You have implemented RAII correctly for the resize operation.
  3. Your push_back leaves the vector in its original state on failure → You have achieved the strong exception guarantee.
  4. Your swap and destructor are marked noexcept → You are writing exception-aware, modern C++.

Project 2: A Thread-Safe Producer-Consumer Queue

  • File: LEARN_ADVANCED_CPP_DEEP_DIVE.md
  • Main Programming Language: C++
  • Alternative Programming Languages: Java, Go (for comparison of concurrency models)
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Concurrency / Multithreading
  • Software or Tool: std::thread, std::mutex, std::condition_variable
  • Main Book: “C++ Concurrency in Action, 2nd Edition” by Anthony Williams

What you’ll build: A templated queue that allows one or more “producer” threads to safely push items onto it, and one or more “consumer” threads to safely pop items from it.

Why it teaches concurrency: This is the “Hello, World!” of practical concurrency. It forces you to deal with the two fundamental problems: race conditions (protecting shared data with a mutex) and CPU waste (avoiding busy-waiting with a condition variable).

Core challenges you’ll face:

  • Protecting shared state → maps to using std::lock_guard or std::unique_lock with a std::mutex
  • Signaling between threads → maps to using std::condition_variable to notify a waiting thread that data is available
  • Handling spurious wakeups → maps to checking a predicate in a while loop when waiting on a condition variable
  • Graceful shutdown → maps to how to tell consumer threads to stop waiting and exit

Key Concepts:

  • Mutexes and Locks: “C++ Concurrency in Action” Ch. 3.
  • Condition Variables: “C++ Concurrency in Action” Ch. 4.
  • Race Conditions: The fundamental problem of concurrent access to shared data.

Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Solid C++ fundamentals.

Real world outcome: A program with multiple threads running, communicating safely through your queue.

// Main function
ThreadSafeQueue<int> queue;

// Producer thread
std::thread producer([&]() {
    for (int i = 0; i < 100; ++i) {
        std::cout << "Pushing " << i << "\n";
        queue.push(i);
        std::this_thread::sleep_for(std::chrono::milliseconds(5));
    }
    queue.shutdown(); // Signal consumers to stop
});

// Consumer thread
std::thread consumer([&]() {
    while (true) {
        auto item = queue.pop();
        if (!item.has_value()) { // Check for shutdown signal
            std::cout << "Consumer shutting down.\n";
            break;
        }
        std::cout << "Popped " << *item << "\n";
    }
});

producer.join();
consumer.join();

Implementation Hints:

template<typename T>
class ThreadSafeQueue {
private:
    std::queue<T> q;
    std::mutex m;
    std::condition_variable cv;
    bool is_shutdown = false;

public:
    void push(T value) {
        std::lock_guard<std::mutex> lock(m);
        q.push(value);
        cv.notify_one(); // Wake up one waiting consumer
    }

    std::optional<T> pop() {
        std::unique_lock<std::mutex> lock(m);
        cv.wait(lock, [&]{ return !q.empty() || is_shutdown; }); // The magic!
        
        if (is_shutdown && q.empty()) {
            return std::nullopt;
        }
        
        T value = q.front();
        q.pop();
        return value;
    }
    
    void shutdown() {
        std::lock_guard<std::mutex> lock(m);
        is_shutdown = true;
        cv.notify_all(); // Wake up all consumers
    }
};

The most important line is cv.wait(lock, predicate). It atomically releases the lock, puts the thread to sleep, and only wakes up when notified and the predicate (!q.empty() || is_shutdown) is true.

Learning milestones:

  1. Your queue works correctly with one producer and one consumer → You have working mutex and condition variable logic.
  2. You can add multiple consumers and producers without data corruption → Your locking is robust.
  3. Consumers don’t burn 100% CPU when the queue is empty → Your condition variable is working efficiently.
  4. The program can shut down cleanly without deadlocks → You have a complete and robust concurrent data structure.

Project 3: A Compile-Time Unit Conversion Library

  • File: LEARN_ADVANCED_CPP_DEEP_DIVE.md
  • Main Programming Language: C++
  • Alternative Programming Languages: None (this is very C++ specific)
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 4: Expert
  • Knowledge Area: Template Metaprogramming (TMP)
  • Software or Tool: C++17 compiler
  • Main Book: “C++ Templates: The Complete Guide, 2nd Edition” by Vandevoorde, Josuttis, and Gregor

What you’ll build: A set of types that encode physical units (meters, kilograms, seconds) into the type system itself. The library will allow you to add meters to meters, but a compile error will occur if you try to add meters to seconds.

Why it teaches TMP: This is a classic, practical application of TMP. It shows how to perform computations (adding/subtracting dimensions) and enforce complex rules at compile time, generating zero-overhead abstractions.

Core challenges you’ll face:

  • Representing dimensions in the type system → maps to using std::ratio or a custom parameter pack of integers
  • Overloading operators with templates → maps to writing operator+ that is only enabled for types with the same dimensions
  • Deriving new units from base units → maps to Velocity = Distance / Time becomes a new type derived at compile time
  • Using static_assert and SFINAE/if constexpr → maps to generating clear error messages for invalid operations

Key Concepts:

  • Template Parameter Packs: For representing the dimensions (M, L, T, ...)
  • std::ratio: For representing fractional exponents of units.
  • if constexpr (C++17): To conditionally compile code based on template parameters.

Difficulty: Expert Time estimate: 2-3 weeks Prerequisites: Strong C++ template knowledge.

Real world outcome: Code that feels like magic and is incredibly safe.

// Define base dimensions using a simple int pack
template<int M, int L, int T> struct Dim {};
using Meters = Value<Dim<0, 1, 0>>;
using Seconds = Value<Dim<0, 0, 1>>;
using Kilograms = Value<Dim<1, 0, 0>>;

// Your library makes this possible
Meters distance(100.0);
Seconds time(10.0);

// Multiplication of values creates a new type!
auto velocity = distance / time; // Type is Value<Dim<0, 1, -1>>
std::cout << velocity.count() << " m/s\n"; // Prints 10.0 m/s

// This line will NOT compile!
// auto invalid_op = distance + time; 
// static_assert failed: "Cannot add values with different dimensions"

Implementation Hints: The core is a Value class templated on the dimension.

template <typename Dimension>
class Value {
    double val;
public:
    explicit Value(double v) : val(v) {}
    double count() const { return val; }
};

// Now, overload operators. This is the hard part.
// Define a helper to add dimensions
template <typename D1, typename D2> struct AddDims; // ...
template <int M1, int L1, int T1, int M2, int L2, int T2>
struct AddDims<Dim<M1, L1, T1>, Dim<M2, L2, T2>> {
    using type = Dim<M1 + M2, L1 + L2, T1 + T2>;
};

// Operator for multiplication
template <typename D1, typename D2>
auto operator*(const Value<D1>& lhs, const Value<D2>& rhs) {
    using NewDim = typename AddDims<D1, D2>::type;
    return Value<NewDim>(lhs.count() * rhs.count());
}

You’ll need similar helpers for division, and then use if constexpr and std::is_same_v to make operator+ only work for identical dimensions.

Learning milestones:

  1. You can define distinct types for meters and seconds → You have a basic dimensional type.
  2. Multiplying/dividing two Values produces a new Value with the correct combined dimension → Your compile-time dimension arithmetic works.
  3. Adding Values with different dimensions fails to compile → Your static assertions or SFINAE constraints are working.
  4. You can define derived units like Velocity and Force → Your system is expressive and practical.

Project 4: A Coroutine-based Redis Client

  • File: LEARN_ADVANCED_CPP_DEEP_DIVE.md
  • Main Programming Language: C++
  • Alternative Programming Languages: Python, Rust (for async/await comparison)
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 5: Master
  • Knowledge Area: Coroutines / Asynchronous Programming / Networking
  • Software or Tool: C++20 compiler, Asio or another async I/O library
  • Main Book: “C++20 - The Complete Guide” by Nicolai M. Josuttis

What you’ll build: A simple, asynchronous client library for the Redis database using C++20 coroutines. The final API should look sequential and be easy to use.

Why it teaches coroutines: This project teaches you how to wrap low-level, callback-based or event-driven I/O into a high-level, clean coroutine interface. This is the primary use case for coroutines in C++. You’ll see how they eliminate “callback hell.”

Core challenges you’ll face:

  • Understanding the coroutine machinery → maps to the roles of promise_type, handle, and the co_await interface
  • Designing an Awaitable type → maps to creating a type that can be co_awaited and interfaces with your I/O backend
  • Managing the lifetime of the coroutine frame → maps to understanding who owns the coroutine state
  • Integrating with an existing I/O library like Asio → maps to bridging the callback world with the coroutine world

Key Concepts:

  • C++20 Coroutines: The syntax (co_await, co_yield, co_return) and the compiler-generated magic.
  • The Awaitable Concept: Any type with await_ready, await_suspend, and await_resume methods.
  • Asynchronous I/O: Event loops (proactors/reactors).

Difficulty: Master Time estimate: 2-4 weeks Prerequisites: Project 2, experience with callbacks or futures, strong C++ skills.

Real world outcome: An async Redis client that is a joy to use.

// This function does multiple network round-trips but looks
// completely linear. No callbacks!
task<std::optional<std::string>> get_and_update_user(int user_id) {
    RedisClient client;
    // co_await suspends this function and lets other work run
    co_await client.connect("localhost", 6379);

    std::string key = "user:" + std::to_string(user_id);
    
    // The result of the async GET command is returned directly
    auto name = co_await client.get(key);
    if (name) {
        std::cout << "Got user: " << *name << "\n";
        co_await client.set(key, *name + "_processed");
    }

    co_return name;
}

// In main:
auto result = co_await get_and_update_user(123);

Implementation Hints: You will need to create a task<T> return type for your coroutines. This is the “future-like” object that represents the eventual result. Inside, it will hold a coroutine_handle.

The hardest part is the Awaitable. For connect, your await_suspend method will call the underlying non-blocking asio::socket::async_connect and pass it a lambda. That lambda’s job is to resume the coroutine handle (handle.resume()) when Asio calls it back.

// Simplified Awaitable for a socket read
struct read_awaitable {
    asio::ip::tcp::socket& socket;
    asio::streambuf& buffer;

    bool await_ready() { return false; } // Always suspend
    void await_resume() {}

    void await_suspend(std::coroutine_handle<> h) {
        // The core logic: start an async op, and in its callback,
        // resume the coroutine handle.
        asio::async_read_until(socket, buffer, '\n',
            [h](const asio::error_code&, std::size_t) {
                h.resume(); // This is the magic link!
            }
        );
    }
};

Learning milestones:

  1. You can co_await a connect operation → You have successfully wrapped a callback-based async call in an awaitable.
  2. You can co_await a get command and receive the result → You are managing coroutine resumption and value passing.
  3. Your task<T> type works correctly → You have a complete future-like coroutine return object.
  4. Your client code is clean, sequential, and fully asynchronous → You have mastered the primary value proposition of C++ coroutines.

Summary

Project Main C++ Topic Difficulty Focus
1. The Exception-Safe Vector Exception Handling Advanced RAII, exception guarantees, noexcept
2. Thread-Safe Queue Concurrency Advanced std::thread, std::mutex, std::condition_variable
3. Compile-Time Units TMP Expert Type system manipulation, static_assert
4. Coroutine Redis Client Coroutines Master Wrapping async I/O in a clean, modern interface