Advanced C++20

Advanced C++20

Overview

C++20 brought the biggest changes since C++11. Revolutionary features such as Concepts, Ranges, Coroutines, and Modules were added. This chapter covers the core features of C++20.

Difficulty: ⭐⭐⭐⭐⭐

Prerequisites: Templates, Lambdas, Smart Pointers


Table of Contents

  1. Concepts
  2. Ranges
  3. Coroutines
  4. Modules
  5. Other C++20 Features
  6. C++23 Preview

Concepts

What are Concepts?

A feature that defines constraints on template parameters. Much more readable than the previous SFINAE approach.

Basic Usage

#include <concepts>
#include <iostream>

// Define a concept
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

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

// Or use requires clause
template<typename T>
    requires Numeric<T>
T multiply(T a, T b) {
    return a * b;
}

// Or trailing requires
template<typename T>
T divide(T a, T b) requires Numeric<T> {
    return a / b;
}

int main() {
    std::cout << add(1, 2) << "\n";        // OK
    std::cout << add(1.5, 2.5) << "\n";    // OK
    // add("hello", "world");              // Compile error!
    return 0;
}

Standard Concepts

#include <concepts>

// Type-related
std::same_as<T, U>           // T and U are the same type
std::derived_from<D, B>      // D derives from B
std::convertible_to<From, To>// From is convertible to To

// Arithmetic-related
std::integral<T>             // Integer type
std::floating_point<T>       // Floating-point type
std::signed_integral<T>      // Signed integer
std::unsigned_integral<T>    // Unsigned integer

// Comparison-related
std::equality_comparable<T>  // == operation possible
std::totally_ordered<T>      // <, >, <=, >= operations possible

// Callable-related
std::invocable<F, Args...>   // F(Args...) is callable
std::predicate<F, Args...>   // F(Args...) returns bool

Custom Concept Definition

#include <concepts>
#include <string>

// Type that behaves like a string
template<typename T>
concept StringLike = requires(T t) {
    { t.length() } -> std::convertible_to<std::size_t>;
    { t.c_str() } -> std::same_as<const char*>;
    { t[0] } -> std::convertible_to<char>;
};

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

// Usage
template<Container C>
void printContainer(const C& container) {
    for (const auto& item : container) {
        std::cout << item << " ";
    }
    std::cout << "\n";
}

Requires Expressions

// Simple requirement
template<typename T>
concept Addable = requires(T a, T b) {
    a + b;  // This expression must be valid
};

// Type requirement
template<typename T>
concept HasValueType = requires {
    typename T::value_type;
};

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

// Nested requirement
template<typename T>
concept Sortable = requires(T t) {
    requires std::totally_ordered<typename T::value_type>;
    { t.begin() } -> std::random_access_iterator;
};

Overloading with Concepts

#include <concepts>
#include <iostream>

template<std::integral T>
void print(T value) {
    std::cout << "Integer: " << value << "\n";
}

template<std::floating_point T>
void print(T value) {
    std::cout << "Float: " << value << "\n";
}

template<typename T>
void print(T value) {
    std::cout << "Other: " << value << "\n";
}

int main() {
    print(42);       // Integer: 42
    print(3.14);     // Float: 3.14
    print("hello");  // Other: hello
    return 0;
}

Ranges

What are Ranges?

A library for handling containers and algorithms more elegantly. Supports pipeline-style operations.

Basic Usage

#include <ranges>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // Traditional way
    // for (auto it = nums.begin(); it != nums.end(); ++it) { ... }

    // Ranges way
    for (int n : nums | std::views::filter([](int x) { return x % 2 == 0; })
                      | std::views::transform([](int x) { return x * x; })) {
        std::cout << n << " ";  // 4 16 36 64 100
    }

    return 0;
}

Views

Views are lazily evaluated and don't copy the original data.

#include <ranges>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // filter: only elements matching condition
    auto evens = v | std::views::filter([](int x) { return x % 2 == 0; });

    // transform: transformation
    auto squared = v | std::views::transform([](int x) { return x * x; });

    // take: first n elements
    auto first3 = v | std::views::take(3);

    // drop: skip first n elements
    auto afterFirst3 = v | std::views::drop(3);

    // reverse: reverse order
    auto reversed = v | std::views::reverse;

    // Combination
    auto result = v | std::views::filter([](int x) { return x > 3; })
                    | std::views::transform([](int x) { return x * 2; })
                    | std::views::take(3);

    for (int n : result) {
        std::cout << n << " ";  // 8 10 12
    }

    return 0;
}

Main Views

#include <ranges>
namespace views = std::views;

// Generator views
auto r1 = views::iota(1, 10);        // 1, 2, ..., 9
auto r2 = views::iota(1) | views::take(10);  // 10 from infinite sequence

// Transform views
auto r3 = v | views::transform(func);
auto r4 = v | views::filter(pred);
auto r5 = v | views::take(n);
auto r6 = v | views::drop(n);
auto r7 = v | views::take_while(pred);
auto r8 = v | views::drop_while(pred);
auto r9 = v | views::reverse;

// Split views
auto r10 = str | views::split(' ');  // Split by space

// Join views
auto r11 = nested | views::join;     // Flatten nested range

// Element views
auto r12 = pairs | views::elements<0>;  // First element of tuple
auto r13 = pairs | views::keys;         // Keys of map
auto r14 = pairs | views::values;       // Values of map

Range Algorithms

#include <ranges>
#include <algorithm>
#include <vector>

int main() {
    std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};

    // Range-based algorithms
    std::ranges::sort(v);
    std::ranges::reverse(v);

    auto it = std::ranges::find(v, 5);
    bool found = std::ranges::contains(v, 5);

    int count = std::ranges::count_if(v, [](int x) { return x > 3; });

    auto [min, max] = std::ranges::minmax(v);

    // Projection
    struct Person {
        std::string name;
        int age;
    };

    std::vector<Person> people = {{"Alice", 30}, {"Bob", 25}};
    std::ranges::sort(people, {}, &Person::age);  // Sort by age

    return 0;
}

Coroutines

What are Coroutines?

Functions that can suspend execution and resume later.

Basic Structure

#include <coroutine>
#include <iostream>

// Coroutine return type
template<typename T>
struct Generator {
    struct promise_type {
        T current_value;

        Generator get_return_object() {
            return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
        }

        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { std::terminate(); }

        std::suspend_always yield_value(T value) {
            current_value = value;
            return {};
        }

        void return_void() {}
    };

    std::coroutine_handle<promise_type> handle;

    explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
    ~Generator() { if (handle) handle.destroy(); }

    Generator(Generator&& other) noexcept : handle(other.handle) {
        other.handle = nullptr;
    }

    bool next() {
        if (!handle.done()) {
            handle.resume();
        }
        return !handle.done();
    }

    T value() const {
        return handle.promise().current_value;
    }
};

// Coroutine function
Generator<int> range(int start, int end) {
    for (int i = start; i < end; ++i) {
        co_yield i;  // Yield value and suspend
    }
}

int main() {
    auto gen = range(1, 5);

    while (gen.next()) {
        std::cout << gen.value() << " ";  // 1 2 3 4
    }

    return 0;
}

co_await, co_yield, co_return

// co_yield: yield a value and suspend
Generator<int> numbers() {
    co_yield 1;
    co_yield 2;
    co_yield 3;
}

// co_return: terminate the coroutine
Task<int> compute() {
    // Async work...
    co_return 42;
}

// co_await: wait on an awaitable object
Task<void> asyncWork() {
    auto result = co_await asyncOperation();
    // Use result...
}

Practical Example: Simple Task

#include <coroutine>
#include <optional>
#include <iostream>

template<typename T>
struct Task {
    struct promise_type {
        std::optional<T> result;

        Task get_return_object() {
            return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }

        std::suspend_never initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { std::terminate(); }

        void return_value(T value) {
            result = value;
        }
    };

    std::coroutine_handle<promise_type> handle;

    Task(std::coroutine_handle<promise_type> h) : handle(h) {}
    ~Task() { if (handle) handle.destroy(); }

    T get() {
        return *handle.promise().result;
    }
};

Task<int> asyncAdd(int a, int b) {
    co_return a + b;
}

int main() {
    auto task = asyncAdd(10, 20);
    std::cout << "Result: " << task.get() << "\n";
    return 0;
}

Modules

What are Modules?

A new code organization method that solves the drawbacks of header files.

Module Definition

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

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

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

// Internal implementation (not exported)
int helper() {
    return 42;
}

Using Modules

// main.cpp
import math;
import <iostream>;

int main() {
    std::cout << add(1, 2) << "\n";
    std::cout << multiply(3, 4) << "\n";
    return 0;
}

Compilation (GCC Example)

# Compile module
g++ -std=c++20 -fmodules-ts -c math.cppm

# Compile and link main
g++ -std=c++20 -fmodules-ts main.cpp math.o -o main

Module Advantages

Traditional Headers Modules
Parsed every time Compiled once
Macro pollution Isolated
Include order matters Order independent
Slow builds Fast builds

Other C++20 Features

Three-way Comparison Operator (Spaceship Operator)

#include <compare>

struct Point {
    int x, y;

    auto operator<=>(const Point&) const = default;
    // ==, !=, <, >, <=, >= automatically generated
};

int main() {
    Point p1{1, 2}, p2{1, 3};

    if (p1 < p2) { /* ... */ }
    if (p1 == p2) { /* ... */ }

    auto result = p1 <=> p2;
    if (result < 0) { /* p1 < p2 */ }

    return 0;
}

Designated Initializers

struct Config {
    int width = 800;
    int height = 600;
    bool fullscreen = false;
    const char* title = "App";
};

int main() {
    // C++20 designated initializers
    Config cfg{
        .width = 1920,
        .height = 1080,
        .fullscreen = true
        // title uses default value
    };

    return 0;
}

consteval and constinit

// consteval: must be evaluated at compile time
consteval int square(int n) {
    return n * n;
}

constexpr int a = square(5);  // OK
// int b = square(x);         // Error! x is not a constant

// constinit: forces static initialization
constinit int global = 42;
// constinit int bad = foo();  // Error! foo() is not constexpr

std::span

#include <span>
#include <vector>
#include <array>

void process(std::span<int> data) {
    for (int& n : data) {
        n *= 2;
    }
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::array<int, 5> stdArr = {1, 2, 3, 4, 5};

    process(arr);      // OK
    process(vec);      // OK
    process(stdArr);   // OK

    return 0;
}

std::format

#include <format>
#include <iostream>

int main() {
    std::string s = std::format("Hello, {}!", "World");
    std::cout << s << "\n";

    std::cout << std::format("{:>10}", 42) << "\n";      // Right align
    std::cout << std::format("{:08x}", 255) << "\n";     // Hex, zero-padded
    std::cout << std::format("{:.2f}", 3.14159) << "\n"; // 2 decimal places

    return 0;
}

std::source_location

#include <source_location>
#include <iostream>

void log(const std::string& msg,
         const std::source_location& loc = std::source_location::current()) {
    std::cout << loc.file_name() << ":"
              << loc.line() << " "
              << loc.function_name() << ": "
              << msg << "\n";
}

int main() {
    log("Hello!");  // main.cpp:15 main: Hello!
    return 0;
}

C++23 Preview

std::expected

#include <expected>
#include <string>

std::expected<int, std::string> divide(int a, int b) {
    if (b == 0) {
        return std::unexpected("Division by zero");
    }
    return a / b;
}

int main() {
    auto result = divide(10, 2);
    if (result) {
        std::cout << "Result: " << *result << "\n";
    } else {
        std::cout << "Error: " << result.error() << "\n";
    }
    return 0;
}

std::print

#include <print>

int main() {
    std::print("Hello, {}!\n", "World");
    std::println("Value: {}", 42);  // Automatic newline
    return 0;
}

std::generator (C++23)

#include <generator>

std::generator<int> range(int start, int end) {
    for (int i = start; i < end; ++i) {
        co_yield i;
    }
}

int main() {
    for (int n : range(1, 10)) {
        std::cout << n << " ";
    }
    return 0;
}

Practice Problems

Problem 1: Define a Concept

Define a Concept representing a "printable" type (supports operator<<).

Show Answer
template<typename T>
concept Printable = requires(std::ostream& os, T t) {
    { os << t } -> std::same_as<std::ostream&>;
};

template<Printable T>
void print(const T& value) {
    std::cout << value << "\n";
}

Problem 2: Range Pipeline

Find the sum of squares of numbers from 1 to 100 that are multiples of 3 but not multiples of 5.

Show Answer
#include <ranges>
#include <numeric>
#include <iostream>

int main() {
    auto result = std::views::iota(1, 101)
        | std::views::filter([](int x) { return x % 3 == 0 && x % 5 != 0; })
        | std::views::transform([](int x) { return x * x; });

    int sum = std::accumulate(result.begin(), result.end(), 0);
    std::cout << "Sum: " << sum << "\n";

    return 0;
}

Next Steps


References

to navigate between lessons