Skip to content
C++
Language
since C++20
Basic

What's New in C++20

"C++20 complete reference: Concepts, Ranges, Coroutines, Modules, std::format, std::span, spaceship operator, consteval, jthread, and key STL additions."

C++20since C++20

The largest C++ revision since C++11, finalized December 2020, delivering four foundational pillars β€” Concepts, Ranges, Coroutines, and Modules β€” alongside dozens of targeted language and library additions.

Overview

C++20 compiler support is toolchain-dependent; check feature-test macros (__cpp_concepts, __cpp_modules, etc.) before relying on specific features in a shared codebase.

cpp
g++ -std=c++20      // GCC 10+ partial; GCC 13+ near-complete
clang++ -std=c++20  // Clang 12+ partial; Clang 16+ near-complete
cl /std:c++20       // MSVC 2019 16.11+ (most features)

The Big Four

Concepts

Concepts replace SFINAE and enable_if with composable, first-class template constraints. They produce far better error messages and enable a vocabulary of named requirements.

cpp
#include <concepts>

template<typename T>
concept Arithmetic = std::is_arithmetic_v<T>;

// Four constraint syntaxes β€” all equivalent in power
template<std::integral T> T add(T a, T b) { return a + b; }     // concept in position

template<typename T> requires std::floating_point<T>             // requires-clause
T reciprocal(T x) { return T{1} / x; }

void process(std::ranges::range auto& r);                        // abbreviated template

template<typename T>
requires requires(T x) { x.size(); x.begin(); x.end(); }        // ad-hoc requires-expression
void dump(const T& c);

// Conjunction / disjunction with && and ||
template<typename T>
concept Sortable = std::ranges::random_access_range<T> &&
                   std::sortable<std::ranges::iterator_t<T>>;

The standard ships ~50 concepts across <concepts> and <iterator>: std::same_as, std::convertible_to, std::invocable, std::regular, std::totally_ordered, std::copyable, etc.

Ranges

The Ranges library (<ranges>) provides composable, lazy pipelines via |. Range algorithms in std::ranges:: accept containers directly β€” no begin()/end() pairs required.

cpp
#include <ranges>

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

// Lazy pipeline β€” no allocation until materialized
auto result = v
    | std::views::filter([](int x){ return x % 2 == 0; })
    | std::views::transform([](int x){ return x * x; })
    | std::views::take(3);
// Yields: 4, 16, 36 β€” evaluated on demand

std::ranges::sort(v);
auto it = std::ranges::find(v, 7);
std::ranges::copy(result, std::back_inserter(out));

// C++20 factory views
std::views::iota(1, 10)         // integers [1, 9]
std::views::iota(0)             // infinite: 0, 1, 2, ...
std::views::single(42)          // range of one element
std::views::empty<int>          // empty typed range

// std::views::zip, std::views::repeat, std::views::chunk β€” C++23, not C++20

Ranges are lazy: passing a view to code expecting std::vector requires explicit materialization via std::ranges::copy or std::ranges::to<std::vector>() (the latter is C++23).

Coroutines

C++20 adds coroutine language machinery via three keywords and <coroutine>. Critically, the standard does not ship a ready-to-use generator or task type β€” only the suspension/resumption protocol. std::generator<T> arrives in C++23.

cpp
#include <coroutine>

// co_yield  β€” produce a value and suspend
// co_await  β€” suspend until an awaitable completes
// co_return β€” finish the coroutine

// With a user-defined Task<T> or library like cppcoro / Asio:
Task<Response> fetch(std::string url) {
    Response r = co_await http_get(url);  // suspend; resume when http_get resolves
    co_return r;
}

// Composing coroutines
Task<std::string> pipeline() {
    auto r = co_await fetch("https://api.example.com/data");
    co_return r.body();
}

Every coroutine type requires a hand-written promise_type implementing get_return_object, initial_suspend, final_suspend, return_value/return_void, and unhandled_exception. Use a tested library for production code rather than writing these from scratch.

Modules

Modules replace #include with a compiled, macro-isolated import mechanism that enables significantly faster incremental builds.

cpp
// math.cppm β€” module interface unit (GCC/Clang: .cppm; MSVC: .ixx by convention)
export module math;

export int add(int a, int b) { return a + b; }
export constexpr double pi = 3.14159265358979;

// Not exported β€” internal linkage within the module
static void impl_detail() {}

// main.cpp
import math;
import <iostream>;  // header unit β€” wraps a legacy header, C++20

int main() {
    std::cout << add(2, 3) << '\n';
}
bash
# Module interface units must be compiled before their consumers
# GCC 11+: -fmodules-ts; Clang 15+: -fmodules; MSVC 2019+: /interface
# Build system support (CMake 3.28+, Ninja) is essential for large codebases

import std; (importing the entire standard library as a module) is C++23, not C++20.


Language Features

Spaceship Operator <=>

cpp
struct Version {
    int major, minor, patch;
    auto operator<=>(const Version&) const = default;  // synthesizes all 6 comparisons
};

// Ordering categories encode the mathematical properties:
// std::strong_ordering   β€” integers, strings (total order, equal implies identical)
// std::weak_ordering     β€” case-insensitive comparison (equal but not identical)
// std::partial_ordering  β€” floats (NaN breaks trichotomy)

struct FloatWrapper {
    float val;
    std::partial_ordering operator<=>(const FloatWrapper& o) const {
        return val <=> o.val;
    }
    bool operator==(const FloatWrapper&) const = default;
};

consteval and constinit

cpp
// consteval β€” immediate function; every call must produce a compile-time constant
consteval int sq(int n) { return n * n; }
constexpr int a = sq(7);   // ok
// int b = sq(runtime_var); // error: argument not a constant expression

// constinit β€” guarantees static/thread_local variables are initialized at compile time
// Does NOT make the variable const; it remains mutable at runtime
constinit int request_count = 0;       // no dynamic init; no SIOF risk
constinit thread_local int tls_id = 0; // per-thread, zero-initialized at compile time

Designated Initializers

cpp
struct ServerConfig {
    std::string host = "localhost";
    uint16_t port    = 8080;
    bool tls         = false;
    int timeout_ms   = 5000;
};

// Designators must appear in declaration order; unmentioned fields keep defaults
ServerConfig cfg{.port = 443, .tls = true};
// cfg.host == "localhost", cfg.port == 443, cfg.tls == true, cfg.timeout_ms == 5000

Other Language Additions

cpp
// Template lambdas β€” explicit type parameters in lambdas
auto first = []<typename T>(std::vector<T>& v) -> T& { return v.front(); };

// [[likely]] / [[unlikely]] β€” attribute on the statement, NOT the condition expression
if (cache_hit) [[likely]] {
    return cache[key];
} else [[unlikely]] {
    return fetch_remote(key);
}

// using enum β€” bring enumerators into local scope
enum class Dir { North, South, East, West };
void move(Dir d) {
    using enum Dir;
    switch (d) { case North: go_north(); break; case South: go_south(); break; }
}

// [[no_unique_address]] β€” empty member optimization (not just bases)
struct Alloc {};
struct Container {
    [[no_unique_address]] Alloc alloc;  // may occupy zero bytes
    int* data;
};

Library Additions

std::span

cpp
#include <span>

void process(std::span<const int> s) {  // non-owning; no copy
    for (int x : s) std::cout << x << ' ';
}

int arr[]{1,2,3,4,5};
process(arr);                         // C array
process(vec);                         // std::vector
process({arr + 1, 3});                // subspan
std::span<int, 3> fixed{arr};         // static-extent span: size known at compile time

std::format

cpp
#include <format>

std::string s = std::format("{:>10.2f}", 3.14);   // "      3.14"
std::string h = std::format("{:#010x}", 255);       // "0x000000ff"

// Custom formatter for user-defined types
template<> struct std::formatter<Point> : std::formatter<std::string> {
    auto format(const Point& p, std::format_context& ctx) const {
        return std::formatter<std::string>::format(
            std::format("({}, {})", p.x, p.y), ctx);
    }
};

std::print and std::println (printing directly to stdout) are C++23, not C++20.

std::jthread and stop_token

cpp
#include <thread>

// Joins automatically on destruction; carries a built-in stop_source
{
    std::jthread worker([](std::stop_token st) {
        while (!st.stop_requested()) { process_item(); }
    });
}  // destructor calls request_stop(), then join() β€” no explicit cleanup needed

std::jthread t(work_fn);
t.request_stop();  // cooperative: the lambda must check st.stop_requested()

Synchronization Primitives

cpp
#include <semaphore>
#include <latch>
#include <barrier>

std::counting_semaphore<8> pool{3};  // template param: max; constructor arg: initial
pool.acquire();                      // blocks when count == 0
pool.release();

std::latch ready{4};         // one-shot: cannot be reset
ready.count_down();          // each of 4 threads calls this
ready.wait();                // blocks until count reaches zero

std::barrier sync{4, []{ swap_buffers(); }};  // reusable; callback runs each phase
sync.arrive_and_wait();      // blocks until all 4 threads arrive, then callback fires

Bit Manipulation and Other Additions

cpp
#include <bit>
std::popcount(0b10110011u);     // 5
std::countl_zero(0b00010000u);  // 3 leading zeros
std::countr_zero(0b00010000u);  // 4 trailing zeros
std::bit_ceil(6u);              // 8 (next power of 2)
std::bit_floor(6u);             // 4 (previous power of 2)
std::has_single_bit(16u);       // true
std::rotl(val, n); std::rotr(val, n);
std::bit_cast<uint32_t>(3.14f); // type-punning without UB
// std::byteswap β€” C++23, not C++20

#include <numbers>
std::numbers::pi;     std::numbers::e;    std::numbers::sqrt2;
std::numbers::phi;    std::numbers::ln2;
std::numbers::pi_v<float>;  // precision-parameterized versions

#include <source_location>
void log(std::string_view msg,
         std::source_location loc = std::source_location::current()) {
    std::cerr << loc.file_name() << ':' << loc.line() << ": " << msg << '\n';
}
log("request received");  // captures caller's file and line, not log()'s

Best Practices

  • Replace enable_if/SFINAE with concepts: Named concepts compose cleanly, subsume each other correctly, and produce meaningful error messages β€” SFINAE chains do none of these.
  • Use std::span<const T> at read-only API boundaries: Replaces (T* data, size_t n) pairs and enables callers to pass arrays, vectors, and subranges without conversion.
  • Adopt std::format for all new string formatting: Type-safe, extensible, locale-independent by default, and immune to buffer overruns.
  • Prefer std::jthread: The auto-join-on-destruction guarantee eliminates a class of bugs in which std::thread calls std::terminate when destroyed joinable.
  • Don't use raw coroutines without a library: promise_type is low-level machinery. Use std::generator (C++23) or a production-quality coroutine library instead.
  • Audit module support per build system: CMake 3.28+ and Ninja 1.11+ have formal module support; older systems require workarounds. Do not adopt modules without validating your full toolchain.

Common Pitfalls

  • constinit β‰  const: constinit only enforces compile-time initialization; the variable is still mutable. Use constexpr for immutable compile-time values.
  • Designated initializer order: Designators must appear in declaration order, matching the struct definition. {.port=443, .host="x"} is ill-formed if host precedes port in the declaration.
  • [[likely]] placement: The attribute annotates the statement, not the condition. if ([[likely]] cond) is ill-formed; the correct form is if (cond) [[likely]] { ... }.
  • Ranges need explicit materialization: Pipelines built with views are lazy. Passing one where a std::vector is expected requires std::ranges::copy or (C++23) std::ranges::to<std::vector>().
  • import std; is C++23: C++20 supports import <iostream>; (header units) but not the blanket import std; shorthand.
  • constexpr allocation must not escape: A constexpr std::vector is valid only if the allocation is fully destroyed within the constant expression β€” you cannot hold a live constexpr std::vector at runtime storage duration.

std::source_location

std::source_location captures call-site metadata at compile time without any preprocessor macros. Prior to C++20, logging and assertion helpers relied on __FILE__, __LINE__, and __func__ macros β€” each with subtly different portability concerns. std::source_location::current() is evaluated where it appears in the source, so using it as a default argument captures the caller's location rather than the logging function's own location.

cpp
// C++20
#include <source_location>
#include <string_view>
#include <iostream>
#include <format>

void log(std::string_view msg,
         std::source_location loc = std::source_location::current()) {
    std::cerr << std::format("[{}:{}] {}: {}\n",
        loc.file_name(), loc.line(), loc.function_name(), msg);
}

// Logging wrapper that propagates location through a call chain
template<typename... Args>
void check(bool cond, std::string_view msg,
           std::source_location loc = std::source_location::current()) {
    if (!cond)
        throw std::runtime_error(std::format("{}:{} check failed: {}",
            loc.file_name(), loc.line(), msg));
}

int main() {
    log("server started");   // prints main.cpp:NN, not log()'s own line
    check(1 + 1 == 2, "math broken");
}

std::source_location provides four accessors: file_name(), line(), column(), and function_name(). The column() value is implementation-defined and may be zero on some toolchains.


std::span β€” Zero-Overhead Contiguous View

std::span<T> is a non-owning, bounds-aware view over any contiguous sequence β€” arrays, vectors, string storage, or raw pointers β€” with zero runtime overhead. It carries a pointer and a length (dynamic extent) or encodes the length in the type (static extent via std::span<T, N>). Prefer it at API boundaries that previously took (T* ptr, size_t n) pairs.

cpp
// C++20
#include <span>
#include <vector>
#include <array>
#include <numeric>

// Single function accepts C arrays, std::array, std::vector, and subranges
double mean(std::span<const double> s) {
    return std::accumulate(s.begin(), s.end(), 0.0) / s.size();
}

int main() {
    double raw[]{1.0, 2.0, 3.0, 4.0, 5.0};
    std::array<double, 3> arr{10.0, 20.0, 30.0};
    std::vector<double>   vec{7.0, 8.0, 9.0};

    mean(raw);              // C array
    mean(arr);              // std::array<double, 3>
    mean(vec);              // std::vector<double>
    mean({raw + 1, 3});     // subspan starting at raw[1], length 3

    // Static extent: size is part of the type
    std::span<double, 5> fixed{raw};     // compile-time size 5
    // std::span<double, 4> bad{raw};    // error: extent mismatch

    // first() / last() / subspan() return new spans
    auto head = std::span{raw}.first(2);   // [1.0, 2.0]
    auto tail = std::span{raw}.last(2);    // [4.0, 5.0]
}

std::span never owns memory and provides no bounds-checked element access by default β€” use at() where you need a throwing accessor, or wrap access in std::mdspan (C++23) for multi-dimensional cases.


Calendar and Timezone in <chrono>

C++20 extends <chrono> with a complete civil-calendar and IANA timezone database, eliminating the need for mktime/gmtime and third-party date libraries for most use cases. The key calendar type is std::chrono::year_month_day; the key timezone type is std::chrono::zoned_time. Both integrate with std::format for locale-aware output.

cpp
// C++20
#include <chrono>
#include <format>
#include <iostream>

int main() {
    using namespace std::chrono;

    // Construct a calendar date with the / operator
    auto today    = year{2026} / month{6} / day{22};
    auto christmas = 2026y / December / 25d;   // using chrono literals

    // Convert to and from system_clock time points
    auto tp = sys_days{christmas};             // midnight UTC on that date
    auto ymd = year_month_day{floor<days>(system_clock::now())};

    // Validity check (e.g. February 30 is invalid)
    if (!ymd.ok()) throw std::runtime_error("invalid date");

    // Timezone-aware time
    auto utc_now  = system_clock::now();
    auto ny_time  = zoned_time{"America/New_York", utc_now};
    auto tok_time = zoned_time{"Asia/Tokyo",       utc_now};

    // Format with std::format (chrono formatters built-in)
    std::cout << std::format("New York : {:%Y-%m-%d %H:%M %Z}\n", ny_time);
    std::cout << std::format("Tokyo    : {:%Y-%m-%d %H:%M %Z}\n", tok_time);
    std::cout << std::format("Christmas: {:%A, %B %d %Y}\n", sys_days{christmas});

    // Duration arithmetic respects calendar rules
    auto next_month = sys_days{year_month_day{ymd.year(), ymd.month() + months{1}, ymd.day()}};
}

The IANA timezone database is shipped with the compiler/stdlib or loaded from the OS (/usr/share/zoneinfo on Linux). On Windows, pass std::chrono::locate_zone("...") or link against the embedded database (MSVC ships it; libstdc++ requires tzdata installed).


Feature-Test Macros

Feature-test macros let you write portable, forward-compatible code that degrades gracefully when specific C++20 features are absent. Each macro is defined in <version> (which every standard header includes) and set to the year-month of the feature's adoption into the standard. Checking them is the recommended alternative to compiler-version guards like #if __GNUC__ >= 11.

cpp
// C++20
#include <version>   // defines all __cpp_* macros without pulling in full headers

// Concepts (language feature)
#if __cpp_concepts >= 201907L
template<std::integral T> T clamp_add(T a, T b);
#else
template<typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
T clamp_add(T a, T b);
#endif

// Coroutines
#ifdef __cpp_coroutines
#  include <coroutine>
#endif

// Library features
#if __cpp_lib_format >= 201907L
#  include <format>
#  define LOG(msg) std::format("[log] {}", msg)
#else
#  define LOG(msg) std::string("[log] ") + (msg)
#endif

#if __cpp_lib_source_location >= 201907L
#  include <source_location>
#endif

#if __cpp_lib_ranges >= 201911L
// safe to use std::views::filter, std::ranges::sort, etc.
#endif

// Enumerate what the current TU supports (useful in build diagnostics)
static_assert(__cpp_consteval >= 201811L, "Need consteval support");
static_assert(__cpp_lib_span  >= 202002L, "Need std::span");

Feature-test macros cover both language features (__cpp_concepts, __cpp_coroutines, __cpp_modules, __cpp_consteval, __cpp_designated_initializers) and library features (__cpp_lib_format, __cpp_lib_ranges, __cpp_lib_jthread, __cpp_lib_semaphore, __cpp_lib_bit_cast). The full table is in the standard's Annex F and mirrored at cppreference.com/w/cpp/feature_test.


See Also

  • Concepts β€” subsumption rules, standard library concept catalog, concept overloading
  • Ranges β€” view composition, range adaptors, sentinel types, range algorithms
  • Coroutines β€” promise_type protocol, custom awaitables, co_await machinery
  • Modules β€” module partitions, header units, export import, build system integration
  • std::format β€” format string syntax, locale variants, writing custom formatters
  • C++17 Features β€” if constexpr, structured bindings, std::optional, std::variant