Chapter 10

Advanced C++

Master modern C++ features: move semantics, templates, concurrency, and beyond

What You Will Learn

  • Move semantics and rvalue references
  • Perfect forwarding for generic code
  • Advanced template techniques
  • SFINAE and type traits
  • C++20 Concepts for cleaner constraints
  • Compile-time programming with constexpr
  • Multithreading with threads, mutexes, atomics
  • Async programming with futures and promises
  • C++20 Modules for faster compilation
  • Coroutines for asynchronous code

Move Semantics

Move semantics allow transferring resources instead of copying.

The Problem: Expensive Copies

std::vector<int> createVector() {
    std::vector<int> v(1000000);  // 1 million elements
    return v;  // Without move: copies all elements!
}

std::vector<int> result = createVector();  // Potentially 2 copies

lvalues vs rvalues

int x = 10;        // x is lvalue (has identity, persistent)
int y = x + 5;     // x + 5 is rvalue (temporary, no identity)

// lvalue reference (binds to lvalues)
int& ref = x;      // OK
// int& ref2 = 10; // Error: can't bind lvalue ref to rvalue

// rvalue reference (binds to rvalues)
int&& rref = 10;   // OK
int&& rref2 = x + 5;  // OK
// int&& rref3 = x;    // Error: x is lvalue

Move Constructor & Move Assignment

class String {
    char* data;
    size_t size;
    
public:
    // Copy constructor (deep copy)
    String(const String& other) 
        : size(other.size), data(new char[other.size]) {
        std::copy(other.data, other.data + size, data);
        std::cout << "Copy\n";
    }
    
    // Move constructor (steal resources)
    String(String&& other) noexcept
        : data(other.data), size(other.size) {
        other.data = nullptr;  // Leave source in valid state
        other.size = 0;
        std::cout << "Move\n";
    }
    
    // Move assignment
    String& operator=(String&& other) noexcept {
        if (this != &other) {
            delete[] data;           // Free existing
            data = other.data;       // Steal
            size = other.size;
            other.data = nullptr;    // Nullify source
            other.size = 0;
        }
        return *this;
    }
    
    ~String() { delete[] data; }
};

std::move

// std::move casts lvalue to rvalue reference
String s1 = "Hello";
String s2 = s1;              // Copy (s1 is lvalue)
String s3 = std::move(s1);   // Move (cast to rvalue)
// s1 is now in "moved-from" state (valid but unspecified)

// Common use: passing to functions
void consume(String s);

String str = "data";
consume(str);              // Copy into function
consume(std::move(str));   // Move into function

Move-Only Types

class UniqueResource {
    int* ptr;
public:
    UniqueResource() : ptr(new int(0)) {}
    ~UniqueResource() { delete ptr; }
    
    // Delete copy operations
    UniqueResource(const UniqueResource&) = delete;
    UniqueResource& operator=(const UniqueResource&) = delete;
    
    // Enable move operations
    UniqueResource(UniqueResource&& other) noexcept 
        : ptr(other.ptr) {
        other.ptr = nullptr;
    }
    
    UniqueResource& operator=(UniqueResource&& other) noexcept {
        if (this != &other) {
            delete ptr;
            ptr = other.ptr;
            other.ptr = nullptr;
        }
        return *this;
    }
};

// std::unique_ptr is the classic example of move-only type

noexcept is Important!

Mark move operations noexcept. STL containers check for noexcept moves and fall back to copying if move can throw.

Perfect Forwarding

Preserve value category (lvalue/rvalue) when passing arguments.

The Forwarding Problem

void process(int& x) { std::cout << "lvalue\n"; }
void process(int&& x) { std::cout << "rvalue\n"; }

// Naive wrapper loses rvalue-ness
template<typename T>
void wrapper(T arg) {
    process(arg);  // Always lvalue! arg has a name
}

wrapper(42);  // Prints "lvalue" (wrong!)

Solution: Forwarding Reference + std::forward

template<typename T>
void wrapper(T&& arg) {  // Forwarding reference (not rvalue ref!)
    process(std::forward<T>(arg));  // Perfect forwarding
}

wrapper(42);       // Prints "rvalue" ✓
int x = 10;
wrapper(x);        // Prints "lvalue" ✓

Reference Collapsing Rules

// T&  &  → T&
// T&  && → T&
// T&& &  → T&
// T&& && → T&&

// When you call wrapper(x) where x is int:
// T deduced as int&
// T&& becomes int& && which collapses to int&

// When you call wrapper(42):
// T deduced as int
// T&& becomes int&&

Factory Pattern with Perfect Forwarding

template<typename T, typename... Args>
std::unique_ptr<T> make_unique_custom(Args&&... args) {
    return std::unique_ptr<T>(
        new T(std::forward<Args>(args)...)
    );
}

// All arguments perfectly forwarded to T's constructor
auto ptr = make_unique_custom<Widget>("name", 42, std::move(data));

Advanced Templates

Variadic Templates

// Parameter pack
template<typename... Args>
void print(Args... args) {
    ((std::cout << args << " "), ...);  // Fold expression (C++17)
    std::cout << "\n";
}

print(1, "hello", 3.14, 'x');  // 1 hello 3.14 x

// sizeof...() gets pack size
template<typename... Ts>
constexpr size_t countArgs() {
    return sizeof...(Ts);
}

static_assert(countArgs<int, double, char>() == 3);

Fold Expressions (C++17)

// Unary right fold: (pack op ...)
template<typename... Args>
auto sum(Args... args) {
    return (args + ...);  // a + (b + (c + d))
}

// Unary left fold: (... op pack)
template<typename... Args>
auto sumLeft(Args... args) {
    return (... + args);  // ((a + b) + c) + d
}

// Binary folds with init value
template<typename... Args>
auto sumWithInit(Args... args) {
    return (0 + ... + args);  // Init value: 0
}

std::cout << sum(1, 2, 3, 4);  // 10

Template Template Parameters

template<template<typename> class Container, typename T>
class Stack {
    Container<T> data;
public:
    void push(const T& value) { data.push_back(value); }
    T pop() {
        T val = data.back();
        data.pop_back();
        return val;
    }
};

Stack<std::vector, int> vectorStack;
Stack<std::deque, int> dequeStack;

CRTP (Curiously Recurring Template Pattern)

// Static polymorphism without virtual functions
template<typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
};

class Derived1 : public Base<Derived1> {
public:
    void implementation() {
        std::cout << "Derived1\n";
    }
};

class Derived2 : public Base<Derived2> {
public:
    void implementation() {
        std::cout << "Derived2\n";
    }
};

template<typename T>
void call(Base<T>& obj) {
    obj.interface();  // No virtual call overhead!
}

SFINAE

Substitution Failure Is Not An Error - enable/disable templates based on conditions.

Type Traits

#include <type_traits>

template<typename T>
void check() {
    if constexpr (std::is_integral_v<T>) {
        std::cout << "Integral type\n";
    } else if constexpr (std::is_floating_point_v<T>) {
        std::cout << "Floating point\n";
    } else {
        std::cout << "Other type\n";
    }
}

// Common type traits:
// is_integral, is_floating_point, is_pointer
// is_reference, is_const, is_class
// is_same, is_base_of, is_convertible
// is_copy_constructible, is_move_constructible

std::enable_if

// Only enable for integral types
template<typename T>
typename std::enable_if<std::is_integral_v<T>, T>::type
add(T a, T b) {
    return a + b;
}

// C++14: enable_if_t shorthand
template<typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
T multiply(T a, T b) {
    return a * b;
}

add(1, 2);        // OK
// add(1.0, 2.0); // Error: no matching function
multiply(1.0, 2.0);  // OK

Detection Idiom

// Check if type has .size() method
template<typename T, typename = void>
struct has_size : std::false_type {};

template<typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>> 
    : std::true_type {};

template<typename T>
void printInfo(const T& container) {
    if constexpr (has_size<T>::value) {
        std::cout << "Size: " << container.size() << "\n";
    } else {
        std::cout << "No size available\n";
    }
}

C++20 Concepts

Cleaner syntax for constraining templates.

Basic Concepts

#include <concepts>

// Define a concept
template<typename T>
concept Numeric = std::is_arithmetic_v<T>;

// Use in template
template<Numeric T>
T add(T a, T b) {
    return a + b;
}

// Alternate syntax
template<typename T>
    requires Numeric<T>
T multiply(T a, T b) {
    return a * b;
}

// Abbreviated function template
auto divide(Numeric auto a, Numeric auto b) {
    return a / b;
}

Standard Concepts

#include <concepts>

// Core concepts
template<std::integral T>
void forIntegers(T val) {}

template<std::floating_point T>
void forFloats(T val) {}

template<std::same_as<std::string> T>
void forStrings(T val) {}

// Comparison concepts
template<std::equality_comparable T>
bool equal(const T& a, const T& b) {
    return a == b;
}

template<std::totally_ordered T>
T maximum(const T& a, const T& b) {
    return (a > b) ? a : b;
}

// Range concepts
template<std::ranges::range R>
void process(R&& range) {
    for (auto&& elem : range) {
        // ...
    }
}

Custom Concepts

// Require specific operations
template<typename T>
concept Drawable = requires(T t) {
    t.draw();              // Must have draw()
    { t.getArea() } -> std::convertible_to<double>;  // getArea returns number
};

template<typename T>
concept Container = requires(T c) {
    typename T::value_type;
    typename T::iterator;
    { c.begin() } -> std::same_as<typename T::iterator>;
    { c.end() } -> std::same_as<typename T::iterator>;
    { c.size() } -> std::convertible_to<std::size_t>;
};

// Compound requirements
template<typename T>
concept Hashable = requires(T a) {
    { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};

constexpr and consteval

Compile-time computation.

constexpr Functions

// Can be evaluated at compile-time if inputs are const
constexpr int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

// Compile-time evaluation
constexpr int result = factorial(5);  // 120
static_assert(factorial(5) == 120);

// Or runtime
int x = 5;
int y = factorial(x);  // Runtime call

consteval (C++20)

// MUST be evaluated at compile-time
consteval int sqr(int n) {
    return n * n;
}

constexpr int a = sqr(5);  // OK: compile-time
// int x = 5; sqr(x);      // Error: not compile-time!

// constinit: compile-time initialization for static/thread_local
constinit int global = factorial(5);  // Compile-time init, mutable at runtime

if constexpr

template<typename T>
auto getValue(T t) {
    if constexpr (std::is_pointer_v<T>) {
        return *t;  // Dereference pointers
    } else {
        return t;   // Return value types directly
    }
}

int x = 42;
int* p = &x;
auto a = getValue(x);   // Returns 42
auto b = getValue(p);   // Returns 42 (dereferenced)

constexpr Containers (C++20)

constexpr std::array<int, 5> arr = {1, 2, 3, 4, 5};
constexpr int sum = std::accumulate(arr.begin(), arr.end(), 0);
static_assert(sum == 15);

// std::vector in constexpr (C++20)
consteval auto makeVector() {
    std::vector<int> v;
    for (int i = 0; i < 5; ++i) {
        v.push_back(i * i);
    }
    return v;
}

constexpr auto vec = makeVector();  // Computed at compile-time

Multithreading

Parallel execution with threads.

std::thread

#include <thread>

void task(int id) {
    std::cout << "Thread " << id << " running\n";
}

int main() {
    std::thread t1(task, 1);
    std::thread t2(task, 2);
    
    t1.join();  // Wait for t1 to finish
    t2.join();  // Wait for t2 to finish
    
    // Or detach (run independently)
    std::thread t3(task, 3);
    t3.detach();  // t3 runs independently
}

Mutexes for Synchronization

#include <mutex>

std::mutex mtx;
int sharedCounter = 0;

void increment(int times) {
    for (int i = 0; i < times; ++i) {
        std::lock_guard<std::mutex> lock(mtx);  // RAII lock
        ++sharedCounter;
    }
}

// Multiple threads can safely increment
std::thread t1(increment, 10000);
std::thread t2(increment, 10000);
t1.join();
t2.join();
// sharedCounter == 20000

Avoiding Deadlocks

std::mutex m1, m2;

void badOrder() {
    std::lock_guard<std::mutex> l1(m1);
    std::lock_guard<std::mutex> l2(m2);  // Potential deadlock!
}

void goodOrder() {
    // Lock both at once
    std::scoped_lock lock(m1, m2);  // C++17 - deadlock-free
}

// Or use unique_lock with std::lock
void alsoGood() {
    std::unique_lock<std::mutex> l1(m1, std::defer_lock);
    std::unique_lock<std::mutex> l2(m2, std::defer_lock);
    std::lock(l1, l2);  // Lock both atomically
}

Condition Variables

#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void worker() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return ready; });  // Wait until ready
    std::cout << "Worker running\n";
}

void signal() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
    }
    cv.notify_one();  // Wake one waiting thread
    // cv.notify_all();  // Wake all waiting threads
}

Atomic Operations

#include <atomic>

std::atomic<int> counter{0};

void increment(int times) {
    for (int i = 0; i < times; ++i) {
        ++counter;  // Atomic increment (no mutex needed)
    }
}

// Atomic flag for spinlock
std::atomic_flag lock = ATOMIC_FLAG_INIT;

void spinlock_acquire() {
    while (lock.test_and_set(std::memory_order_acquire)) {
        // Spin
    }
}

void spinlock_release() {
    lock.clear(std::memory_order_release);
}

Async and Futures

Higher-level asynchronous programming.

std::async

#include <future>

int compute(int x) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return x * x;
}

int main() {
    // Launch async task
    std::future<int> result = std::async(std::launch::async, compute, 10);
    
    // Do other work...
    std::cout << "Computing...\n";
    
    // Get result (blocks if not ready)
    int value = result.get();  // 100
    std::cout << "Result: " << value << "\n";
}

Launch Policies

// async: Run in new thread
auto f1 = std::async(std::launch::async, task);

// deferred: Run when get() is called (lazy)
auto f2 = std::async(std::launch::deferred, task);

// Default: Implementation decides
auto f3 = std::async(task);

std::promise and std::future

void producer(std::promise<int>& prom) {
    int result = 42;  // Compute something
    prom.set_value(result);  // Fulfill promise
}

void consumer(std::future<int>& fut) {
    int value = fut.get();  // Wait for value
    std::cout << "Got: " << value << "\n";
}

int main() {
    std::promise<int> prom;
    std::future<int> fut = prom.get_future();
    
    std::thread prod(producer, std::ref(prom));
    std::thread cons(consumer, std::ref(fut));
    
    prod.join();
    cons.join();
}

std::packaged_task

// Wrap callable for async execution
std::packaged_task<int(int, int)> task([](int a, int b) {
    return a + b;
});

std::future<int> result = task.get_future();

std::thread t(std::move(task), 2, 3);

int sum = result.get();  // 5
t.join();

C++20 Modules

Modern replacement for #include headers.

Module Interface

// math.cppm (module interface unit)
export module math;

export int add(int a, int b) {
    return a + b;
}

export int multiply(int a, int b) {
    return a * b;
}

// Non-exported (internal)
int helper(int x) {
    return x * x;
}

Using Modules

// main.cpp
import math;

int main() {
    int sum = add(3, 4);       // OK
    int prod = multiply(2, 5); // OK
    // helper(5);              // Error: not exported
}

Module Partitions

// math-impl.cppm (partition)
export module math:impl;

export int addImpl(int a, int b) {
    return a + b;
}

// math.cppm (primary interface)
export module math;
export import :impl;  // Re-export partition

export int add(int a, int b) {
    return addImpl(a, b);
}

Benefits

  • Faster compilation: Modules are compiled once
  • No header guards needed: No multiple inclusion
  • Better encapsulation: Only exported symbols visible
  • No macro leakage: Macros don't leak across modules

C++20 Coroutines

Functions that can suspend and resume execution.

Generator Example

#include <coroutine>
#include <iostream>

template<typename T>
struct Generator {
    struct promise_type {
        T current_value;
        
        Generator get_return_object() {
            return Generator{Handle::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        std::suspend_always yield_value(T value) {
            current_value = value;
            return {};
        }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
    
    using Handle = std::coroutine_handle<promise_type>;
    Handle handle;
    
    Generator(Handle h) : handle(h) {}
    ~Generator() { if (handle) handle.destroy(); }
    
    T next() {
        handle.resume();
        return handle.promise().current_value;
    }
    
    bool done() { return handle.done(); }
};

Using the Generator

Generator<int> range(int start, int end) {
    for (int i = start; i < end; ++i) {
        co_yield i;  // Suspend and yield value
    }
}

int main() {
    auto gen = range(0, 5);
    while (!gen.done()) {
        std::cout << gen.next() << " ";
    }
    // Output: 0 1 2 3 4
}

Async Coroutine

// Simplified async/await pattern
Task<int> fetchData(std::string url) {
    auto response = co_await asyncHttpGet(url);
    auto data = co_await parseJson(response);
    co_return data.getValue();
}

// co_await suspends until result is ready
// co_return returns value from coroutine

Coroutine Keywords

  • co_await - Suspend until awaitable is ready
  • co_yield - Suspend and yield a value
  • co_return - Complete and return final value

Practice Questions

  1. Implement a move-only class and demonstrate move semantics.
  2. Create a perfect forwarding factory function for any type.
  3. Write a variadic template function that prints all arguments.
  4. Use SFINAE to create a function that only accepts containers with .size().
  5. Define a C++20 concept for "Addable" types and use it.
  6. Write constexpr functions for compile-time string manipulation.
  7. Create a thread pool that processes tasks from a queue.
  8. Use std::async to parallelize a CPU-intensive computation.
  9. Implement a producer-consumer pattern with condition variables.
  10. Create a simple generator coroutine for Fibonacci numbers.
  11. Use atomic operations to implement a lock-free counter.
  12. Build a module-based library (if your compiler supports modules).
  13. Implement the CRTP pattern for static polymorphism.
  14. Use std::jthread (C++20) with stop tokens for cooperative cancellation.