Skip to content

C++20: The Big Four & More

Discover C++20's transformative features: concepts, ranges, the spaceship operator, std::format, designated initialisers, std::span, and more.

C++20: A Generational Leap

C++20 is the most significant update to C++ since C++11. It introduces four major features — Concepts, Ranges, Coroutines, and Modules — each of which fundamentally changes how C++ code is written. Beyond the "Big Four," C++20 also brings the spaceship operator, std::format, std::span, designated initialisers, and many constexpr enhancements. This lesson covers everything except Coroutines and Modules, which have dedicated lessons, as they would hate to feel left out.

Concepts: Constraining Templates

Concepts replace SFINAE with readable, composable constraints on template parameters. They produce clear error messages when constraints are not satisfied, which is a lot more polite than what came before:

concepts.cpp
#include <iostream>
#include <concepts>
#include <string>
#include <vector>
#include <numeric>

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

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

// Shorthand syntax — concept as type constraint
auto multiply(Numeric auto a, Numeric auto b) {
    return a * b;
}

// Concept with multiple requirements
template<typename Container>
concept Summable = requires(Container c) {
    { c.begin() } -> std::input_or_output_iterator;
    { c.end() } -> std::input_or_output_iterator;
    { c.size() } -> std::convertible_to<std::size_t>;
};

template<Summable Container>
auto sum_container(const Container& c) {
    using T = typename Container::value_type;
    return std::accumulate(c.begin(), c.end(), T{});
}

int main() {
    std::cout << add(3, 4) << "\n";           // 7
    std::cout << multiply(2.5, 4.0) << "\n";  // 10.0

    std::vector<int> v{1, 2, 3, 4, 5};
    std::cout << sum_container(v) << "\n";     // 15

    // add(std::string("a"), std::string("b"));  // Compile error!
    // Error message: constraints not satisfied — Numeric<std::string> is false

    return 0;
}

Ranges: Composable Algorithms

Ranges bring a functional, pipeline-style approach to the STL. Views are lazy: they don't allocate or copy data, composing transformations that execute only when iterated. Rather like a teenager, then:

ranges.cpp
#include <iostream>
#include <ranges>
#include <vector>
#include <string>
#include <algorithm>

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

    // Pipeline: filter even, square them, take first 3
    auto result = numbers
        | std::views::filter([](int n) { return n % 2 == 0; })
        | std::views::transform([](int n) { return n * n; })
        | std::views::take(3);

    for (int n : result) {
        std::cout << n << " ";  // 4 16 36
    }
    std::cout << "\n";

    // Range-based algorithms with projections
    struct Employee {
        std::string name;
        int salary;
    };

    std::vector<Employee> team{
        {"Alice", 90000}, {"Bob", 75000}, {"Carol", 120000}
    };

    // Sort by salary using a projection — no custom comparator needed
    std::ranges::sort(team, {}, &Employee::salary);
    for (const auto& e : team) {
        std::cout << e.name << ": " << e.salary << "\n";
    }

    // iota generates an infinite sequence
    for (int i : std::views::iota(1) | std::views::take(5)) {
        std::cout << i << " ";  // 1 2 3 4 5
    }
    std::cout << "\n";

    return 0;
}

Three-Way Comparison & Designated Initialisers

The spaceship operator <=> generates all six comparison operators from a single declaration. Designated initialisers bring named field initialisation from C to C++ with added type safety:

spaceship.cpp
#include <iostream>
#include <compare>
#include <string>

struct Version {
    int major;
    int minor;
    int patch;

    // One operator generates ==, !=, <, >, <=, >=
    auto operator<=>(const Version&) const = default;
};

struct Config {
    std::string host = "localhost";
    int port = 8080;
    bool ssl = false;
    int max_connections = 100;
};

int main() {
    Version v1{2, 0, 0};
    Version v2{1, 9, 5};

    if (v1 > v2) std::cout << "v1 is newer\n";  // true
    if (v1 != v2) std::cout << "versions differ\n";

    // Strong ordering gives you all 6 operators for free
    auto cmp = v1 <=> v2;
    if (cmp > 0) std::cout << "v1 > v2\n";

    // Designated initializers — name the fields you're setting
    Config cfg{
        .host = "example.com",
        .port = 443,
        .ssl = true
        // .max_connections keeps its default value (100)
    };

    std::cout << cfg.host << ":" << cfg.port
              << " SSL=" << std::boolalpha << cfg.ssl
              << " max=" << cfg.max_connections << "\n";

    return 0;
}

std::format & std::span

std::format brings Python-like string formatting to C++. std::span is a non-owning view over a contiguous sequence of elements, replacing raw pointer+size pairs:

format_span.cpp
#include <iostream>
#include <format>
#include <span>
#include <vector>
#include <array>

// std::span — works with any contiguous container
void print_values(std::span<const int> data) {
    for (int v : data) {
        std::cout << std::format("{:>5}", v);
    }
    std::cout << "\n";
}

int main() {
    // std::format — type-safe, positional formatting
    std::string msg = std::format("Hello, {}! You are {} years old.", "Alice", 30);
    std::cout << msg << "\n";

    // Format specifiers
    std::cout << std::format("Hex: {:#x}, Oct: {:#o}, Bin: {:#b}\n", 255, 255, 255);
    std::cout << std::format("Pi: {:.4f}\n", 3.14159265);
    std::cout << std::format("{:<15} {:>10}\n", "Name", "Score");
    std::cout << std::format("{:<15} {:>10}\n", "Alice", 95);

    // std::span — unifies arrays, vectors, and raw buffers
    int c_array[] = {1, 2, 3, 4, 5};
    std::vector<int> vec{6, 7, 8, 9, 10};
    std::array<int, 3> arr{11, 12, 13};

    print_values(c_array);  // works with C array
    print_values(vec);      // works with vector
    print_values(arr);      // works with std::array

    // Subspan — slice without copying
    std::span<int> full(vec);
    auto first_three = full.subspan(0, 3);
    print_values(first_three);  //  6  7  8

    return 0;
}

constexpr Enhancements & Chrono

C++20 massively expands what can be constexpr: virtual functions, dynamic_cast, try-catch, and even std::vector and std::string can now be used in constexpr contexts. This means more computation moves to compile time, resulting in faster runtime code. The kettle, as it were, boils before the guests arrive.

The library gains a full calendar and time zone system. You can represent dates like 2024y/January/15, do calendar arithmetic, and convert between time zones — all type-safe and without external libraries.

Attributes: [[likely]] and [[unlikely]] hint to the compiler which branches are hot paths, enabling better code generation in performance-critical sections.

Best Practice

You don't have to use all of C++20 at once. Start with the features that give the biggest productivity boost with the least disruption:

1. Spaceship operator — add auto operator<=>(const T&) const = default; to your types immediately
2. Concepts — use standard concepts (std::integral, std::ranges::range) before writing custom ones
3. std::format — replace printf and stringstream formatting
4. Ranges — start with std::views::filter and std::views::transform in new code
5. Designated initialisers — use for configuration structs and option types

Save Coroutines and Modules for when your team and toolchain are ready. No one has ever regretted a quiet, gradual migration.

Key Takeaways
  • Concepts replace SFINAE with readable, composable template constraints and clear error messages
  • Ranges provide lazy, composable pipelines using the | operator — no temporary containers needed
  • The spaceship operator <=> generates all six comparison operators from one defaulted declaration
  • std::format brings type-safe, Python-style string formatting to C++
  • std::span is a non-owning view over contiguous memory, replacing pointer+size pairs
  • Designated initialisers ({.field = value}) make struct construction self-documenting

Quiz — Test Your Knowledge

(20 XP)

1. What does `auto operator<=>(const MyType&) const = default;` do?

2. What is the key benefit of Ranges views (like `std::views::filter`)?

3. What problem do C++20 Concepts solve?