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++20The 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.
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.
#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.
#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++20Ranges 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.
#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.
// 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';
}# 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 codebasesimport std; (importing the entire standard library as a module) is C++23, not C++20.
Language Features
Spaceship Operator <=>
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
// 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 timeDesignated Initializers
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 == 5000Other Language Additions
// 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
#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 timestd::format
#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
#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
#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 firesBit Manipulation and Other Additions
#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()'sBest 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::formatfor 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 whichstd::threadcallsstd::terminatewhen destroyed joinable. - Don't use raw coroutines without a library:
promise_typeis low-level machinery. Usestd::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:constinitonly enforces compile-time initialization; the variable is still mutable. Useconstexprfor immutable compile-time values.- Designated initializer order: Designators must appear in declaration order, matching the struct definition.
{.port=443, .host="x"}is ill-formed ifhostprecedesportin the declaration. [[likely]]placement: The attribute annotates the statement, not the condition.if ([[likely]] cond)is ill-formed; the correct form isif (cond) [[likely]] { ... }.- Ranges need explicit materialization: Pipelines built with views are lazy. Passing one where a
std::vectoris expected requiresstd::ranges::copyor (C++23)std::ranges::to<std::vector>(). import std;is C++23: C++20 supportsimport <iostream>;(header units) but not the blanketimport std;shorthand.- constexpr allocation must not escape: A
constexpr std::vectoris valid only if the allocation is fully destroyed within the constant expression β you cannot hold a liveconstexpr std::vectorat 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.
// 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.
// 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.
// 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.
// 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