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 readyco_yield- Suspend and yield a valueco_return- Complete and return final value
Practice Questions
- Implement a move-only class and demonstrate move semantics.
- Create a perfect forwarding factory function for any type.
- Write a variadic template function that prints all arguments.
- Use SFINAE to create a function that only accepts containers with .size().
- Define a C++20 concept for "Addable" types and use it.
- Write constexpr functions for compile-time string manipulation.
- Create a thread pool that processes tasks from a queue.
- Use std::async to parallelize a CPU-intensive computation.
- Implement a producer-consumer pattern with condition variables.
- Create a simple generator coroutine for Fibonacci numbers.
- Use atomic operations to implement a lock-free counter.
- Build a module-based library (if your compiler supports modules).
- Implement the CRTP pattern for static polymorphism.
- Use std::jthread (C++20) with stop tokens for cooperative cancellation.