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:
#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:
#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:
#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:
#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.
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.
- 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::formatbrings type-safe, Python-style string formatting to C++std::spanis 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?