C++23/20/17/14/11

Snapshot 2026-08-04 12:19:46 UTC · version 1

published
M
MDRSS Source Library Github collector404 cards · 0.0/10 MDRSS

Some examples of code snippets that were broken pre-C++23 that are now fixed: A stacktrace is an approximate representation of an invocation sequence and consists of stacktrace entries. A stacktrace entry (represented by std::stacktraceentry) consists of information including the source file and line number, and a description field.

software-craft/languages-and-idiomstype:guide#cpp#cpp11#cpp14#cpp17#cpp20#cpp23
MARKDOWN SNAPSHOT

Loading…

Direct .mdRaw + metadata0 commentsMDRSS 0.0/10
INDEXABLE MARKDOWN SNAPSHOT

Research document

Open canonical .md

C++23/20/17/14/11

Overview

C++23 includes the following new language features:

C++23 includes the following new library features:

C++20 includes the following new language features:

C++20 includes the following new library features:

C++17 includes the following new language features:

C++17 includes the following new library features:

C++14 includes the following new language features:

C++14 includes the following new library features:

C++11 includes the following new language features:

C++11 includes the following new library features:

C++23 Language Features

consteval if

Write code that is instantiated during constant evaluation.

consteval int f(int i) { return i; }

constexpr int g(int i) {
  if consteval {
      return f(i);
  } else {
      return 42;
  }
}

Deducing this

Using explicit object member functions introduced in C++23, deducing the object's type and value category is now possible by specifying the first parameter of a member function prefixed with the this keyword:

// NEW WAY USING DEDUCING THIS:
struct T {
  decltype(auto) operator[](https://github.com/AnthonyCalandra/modern-cpp-features/blob/HEAD/this auto& self, std::size_t idx) { 
    return self.mVector[idx]; 
  }
};

// OLD WAY:
struct T {
  value_t& operator[](https://github.com/AnthonyCalandra/modern-cpp-features/blob/HEAD/std::size_t idx) {
    return mVector[idx];
  }
  const value_t& operator[](https://github.com/AnthonyCalandra/modern-cpp-features/blob/HEAD/std::size_t idx) const {
    return mVector[idx];
  }
};

Multidimensional subscript operator

Specify zero or more arguments to the operator[] operator:

template <typename T, std::size_t Z, std::size_t Y, std::size_t X>
struct Array3d {
  std::array<T, X * Y * Z> m{};

  T& operator[](https://github.com/AnthonyCalandra/modern-cpp-features/blob/HEAD/std::size_t z, std::size_t y, std::size_t x) {
      return m[z * Y * X + y * X + x];
  }
};

Array3d<int, 4, 3, 2> v;
v[3, 2, 1] = 42;

Increasing range-based for safety

Fixes some of the notorious lifetime issues with one of the most important control structures in C++.

Some examples of code snippets that were broken pre-C++23 that are now fixed:

  • for (auto e : getTmp().getRef())
  • for (auto e : getVector()[0])
  • for (auto valueElem : getMap()["key"])
  • for (auto e : get<0>(getTuple()))
  • for (auto e : getOptionalCollection().value())
  • for (char c : get<std::string>(getVariant()))

C++23 Library Features

Stacktrace library

A stacktrace is an approximate representation of an invocation sequence and consists of stacktrace entries. A stacktrace entry (represented by std::stacktrace_entry) consists of information including the source file and line number, and a description field.

Example output on a Linux system:

#include <print>
#include <stacktrace>

int main() {
    std::println("{}", std::stacktrace::current());
}
  0#  main at /app/example.cpp:5 [0x5ee42e3db747]
  1#  <unknown> [0x76e76dc29d8f]
  2#  __libc_start_main [0x76e76dc29e3f]
  3#  _start [0x5ee42e3db644]

contains for strings and string views

A simpler function for querying if a substring is contained within a string or string view:

std::string{"foobarbaz"}.contains("bar"); // == true
std::string{"foobarbaz"}.contains("bat"); // == false

std::to_underlying

Supports the common utility of converting an enumeration to its underlying type:

enum class MyEnum : int { A = 1, B, C };
std::to_underlying(MyEnum::A); // == 1
std::to_underlying(MyEnum::C); // == 3

spanstream

A strstream replacement using a character span as an externally-provided buffer. No ownership or re-allocation on the buffer.

char input[] = "10 20 30";
std::ispanstream is{std::span<char>{input}};
int i;
is >> i; // i == 10
is >> i; // i == 20
is >> i; // i == 30
char output[30]{}; // zero-initialize array
std::ospanstream os{std::span<char>{output}};
os << 10 << 20 << 30;
std::span<char> sp = os.span();

Input/output pointers

std::out_ptr and std::inout_ptr are abstractions to support both C APIs and smart pointers by creating a temporary pointer-to-pointer that updates the smart pointer when it destructs. In short: it's a thing convertible to a T** that updates (with a reset call or semantically equivalent behavior) the smart pointer it is created with when it goes out of scope.

This abstraction also safely manages the lifetime of the associated memory when exceptions are thrown.

// p_handle is written (out) to.
int c_api_create_handle(MyHandle** p_handle);
// p_handle is both read (in) and written (out) to.
int c_api_recreate_handle(MyHandle** p_handle);
void c_api_delete_handle(MyHandle* handle);

struct resource_deleter {
	void operator()(MyHandle* handle) {
		c_api_delete_handle(handle);
	}
};
std::unique_ptr<MyHandle, resource_deleter> resource(nullptr);
int err = c_api_create_handle(std::out_ptr(resource));
// `resource` now owns the memory allocated within `c_api_create_handle`.
std::shared_ptr<MyHandle> resource(nullptr);
int err = c_api_recreate_handle(std::inout_ptr(resource), resource_deleter{});
// `resource` now shares the memory allocated within `c_api_recreate_handle`.

Both inout/out pointers support casts to void** (implicitly), and explicitly to user-specified types.

Monadic operations for std::optional

Support various and_then, transform, and or_else operations for std::optional.

std::optional<int> parse_int(const std::string&);
std::optional<int> ensure_non_negative(int);
std::optional<double> default_value_or_empty(double);

std::optional<double> stringToSqrtDouble(const std::string& input) {
  return parse_int(input)
    .and_then(ensure_non_negative)
    .transform([](https://github.com/AnthonyCalandra/modern-cpp-features/blob/HEAD/int x) {
      return std::sqrt(x);
    })
    .or_else(default_value_or_empty);
}

std::expected

std::expected provides a way to represent a value and a potential error value, both contained in one type. Also supports a variety of monadic operations on both the expected and unexpected (i.e. error) values.

Use std::unexpected to store an unexpected (i.e. error) value.

enum class StringToSqrtDoubleError {
    ParseError, NegativeNumber
};

std::expected<int, StringToSqrtDoubleError> parse_int(const std::string&);

std::expected<double, StringToSqrtDoubleError> stringToSqrtDouble(const std::string& input) {
    auto parsed = parse_int(input);
    if (!parsed) return parsed;

    auto parsedInt = *parsed;
    if (parsedInt < 0) return std::unexpected(StringToSqrtDoubleError::NegativeNumber);

    return std::sqrt(parsedInt);
}

std::unreachable

Provides a way to explicitly mark a code path as unreachable. May exhibit undefined behavior if the code path is reached.

enum class MyEnum { A, B, C };

int convertMyEnumToInt(MyEnum e) {
    switch (e) {
        case MyEnum::A: return 0;
        case MyEnum::B: return 1;
        case MyEnum::C: return 2;
        default: std::unreachable(); 
    }
}

C++20 Language Features

Coroutines

Note: While these examples illustrate how to use coroutines at a basic level, there is lots more going on when the code is compiled. These examples are not meant to be complete coverage of C++20's coroutines. Since the generator and task classes are not provided by the standard library yet, I used the cppcoro library to compile these examples.

Coroutines are special functions that can have their execution suspended and resumed. To define a coroutine, the co_return, co_await, or co_yield keywords must be present in the function's body. C++20's coroutines are stackless; unless optimized out by the compiler, their state is allocated on the heap.

An example of a coroutine is a generator function, which yields (i.e. generates) a value at each invocation:

generator<int> range(int start, int end) {
  while (start < end) {
    co_yield start;
    start++;
  }

  // Implicit co_return at the end of this function:
  // co_return;
}

for (int n : range(0, 10)) {
  std::cout << n << std::endl;
}

The above range generator function generates values starting at start until end (exclusive), with each iteration step yielding the current value stored in start. The generator maintains its state across each invocation of range (in this case, the invocation is for each iteration in the for loop). co_yield takes the given expression, yields (i.e. returns) its value, and suspends the coroutine at that point. Upon resuming, execution continues after the co_yield.

Another example of a coroutine is a task, which is an asynchronous computation that is executed when the task is awaited:

task<void> echo(socket s) {
  for (;;) {
    auto data = co_await s.async_read();
    co_await async_write(s, data);
  }

  // Implicit co_return at the end of this function:
  // co_return;
}

In this example, the co_await keyword is introduced. This keyword takes an expression and suspends execution if the thing you're awaiting on (in this case, the read or write) is not ready, otherwise you continue execution. (Note that under the hood, co_yield uses co_await.)

Using a task to lazily evaluate a value:

task<int> calculate_meaning_of_life() {
  co_return 42;
}

auto meaning_of_life = calculate_meaning_of_life();
// ...
co_await meaning_of_life; // == 42

Concepts

Concepts are named compile-time predicates which constrain types. They take the following form:

template < template-parameter-list >
concept concept-name = constraint-expression;

where constraint-expression evaluates to a constexpr Boolean. Constraints should model semantic requirements, such as whether a type is a numeric or hashable. A compiler error results if a given type does not satisfy the concept it's bound by (i.e. constraint-expression returns false). Because constraints are evaluated at compile-time, they can provide more meaningful error messages and runtime safety.

// `T` is not limited by any constraints.
template <typename T>
concept always_satisfied = true;
// Limit `T` to integrals.
template <typename T>
concept integral = std::is_integral_v<T>;
// Limit `T` to both the `integral` constraint and signedness.
template <typename T>
concept signed_integral = integral<T> && std::is_signed_v<T>;
// Limit `T` to both the `integral` constraint and the negation of the `signed_integral` constraint.
template <typename T>
concept unsigned_integral = integral<T> && !signed_integral<T>;

There are a variety of syntactic forms for enforcing concepts:

// Forms for function parameters:
// `T` is a constrained type template parameter.
template <my_concept T>
void f(T v);

// `T` is a constrained type template parameter.
template <typename T>
  requires my_concept<T>
void f(T v);

// `T` is a constrained type template parameter.
template <typename T>
void f(T v) requires my_concept<T>;

// `v` is a constrained deduced parameter.
void f(my_concept auto v);

// `v` is a constrained non-type template parameter.
template <my_concept auto v>
void g();

// Forms for auto-deduced variables:
// `foo` is a constrained auto-deduced value.
my_concept auto foo = ...;

// Forms for lambdas:
// `T` is a constrained type template parameter.
auto f = []<my_concept T> (T v) {
  // ...
};
// `T` is a constrained type template parameter.
auto f = []<typename T> requires my_concept<T> (T v) {
  // ...
};
// `T` is a constrained type template parameter.
auto f = []<typename T> (T v) requires my_concept<T> {
  // ...
};
// `v` is a constrained deduced parameter.
auto f = [](https://github.com/AnthonyCalandra/modern-cpp-features/blob/HEAD/my_concept auto v) {
  // ...
};
// `v` is a constrained non-type template parameter.
auto g = []<my_concept auto v> () {
  // ...
};

The requires keyword is used either to start a requires clause or a requires expression:

template <typename T>
  requires my_concept<T> // `requires` clause.
void f(T);

template <typename T>
concept callable = requires (T f) { f(); }; // `requires` expression.

template <typename T>
  requires requires (T x) { x + x; } // `requires` clause and expression on same line.
T add(T a, T b) {
  return a + b;
}

Note that the parameter list in a requires expression is optional. Each requirement in a requires expression are one of the following:

  • Simple requirements - asserts that the given expression is valid.
template <typename T>
concept callable = requires (T f) { f(); };
  • Type requirements - denoted by the typename keyword followed by a type name, asserts that the given type name is valid.
struct foo {
  int foo;
};

struct bar {
  using value = int;
  value data;
};

struct baz {
  using value = int;
  value data;
};

// Using SFINAE, enable if `T` is a `baz`.
template <typename T, typename = std::enable_if_t<std::is_same_v<T, baz>>>
struct S {};

template <typename T>
using Ref = T&;

template <typename T>
concept C = requires {
                     // Requirements on type `T`:
  typename T::value; // A) has an inner member named `value`
  typename S<T>;     // B) must have a valid class template specialization for `S`
  typename Ref<T>;   // C) must be a valid alias template substitution
};

template <C T>
void g(T a);

g(foo{}); // ERROR: Fails requirement A.
g(bar{}); // ERROR: Fails requirement B.
g(baz{}); // PASS.
  • Compound requirements - an expression in braces followed by a trailing return type or type constraint.
template <typename T>
concept C = requires(T x) {
  {*x} -> std::convertible_to<typename T::inner>; // the type of the expression `*x` is convertible to `T::inner`
  {x + 1} -> std::same_as<int>; // the expression `x + 1` satisfies `std::same_as<decltype((x + 1))>`
  {x * 1} -> std::convertible_to<T>; // the type of the expression `x * 1` is convertible to `T`
};
  • Nested requirements - denoted by the requires keyword, specify additional constraints (such as those on local parameter arguments).
template <typename T>
concept C = requires(T x) {
  requires std::same_as<sizeof(x), size_t>;
};

See also: concepts library.

Three-way comparison

C++20 introduces the spaceship operator (<=>) as a new way to write comparison functions that reduce boilerplate and help developers define clearer comparison semantics. Defining a three-way comparison operator will autogenerate the other comparison operator functions (i.e. ==, !=, <, etc.).

Three orderings are introduced:

  • std::strong_ordering: The strong ordering distinguishes between items being equal (identical and interchangeable). Provides less, greater, equivalent, and equal ordering. Examples of comparisons: searching for a specific value in a list, values of integers, case-sensitive strings.
  • std::weak_ordering: The weak ordering distinguishes between items being equivalent (not identical, but can be interchangeable for the purposes of comparison). Provides less, greater, and equivalent ordering. Examples of comparisons: case-insensitive strings, sorting, comparing some but not all visible members of a class.
  • std::partial_ordering: The partial ordering follows the same principle of weak ordering but includes the case when an ordering isn't possible. Provides less, greater, equivalent, and unordered ordering. Examples of comparisons: floating-point values (e.g. NaN).

A defaulted three-way comparison operator does a member-wise comparison:

struct foo {
  int a;
  bool b;
  char c;

  // Compare `a` first, then `b`, then `c` ...
  friend auto operator<=>(const foo&) const = default;
};

foo f1{0, false, 'a'}, f2{0, true, 'b'};
f1 < f2; // == true
f1 == f2; // == false
f1 >= f2; // == false

You can also define your own comparisons:

struct foo {
  int x;
  bool b;
  char c;

  friend std::strong_ordering operator<=>(const foo& other) const {
      return x <=> other.x;
  }
};

foo f1{0, false, 'a'}, f2{0, true, 'b'};
f1 < f2; // == false
f1 == f2; // == true
f1 >= f2; // == true

Designated initializers

C-style designated initializer syntax. Any member fields that are not explicitly listed in the designated initializer list are default-initialized.

struct A {
  int x;
  int y;
  int z = 123;
};

A a {.x = 1, .z = 2}; // a.x == 1, a.y == 0, a.z == 2

Template syntax for lambdas

Use familiar template syntax in lambda expressions.

auto f = []<typename T>(std::vector<T> v) {
  // ...
};

Range-based for loop with initializer

This feature simplifies common code patterns, helps keep scopes tight, and offers an elegant solution to a common lifetime problem.

for (auto v = std::vector{1, 2, 3}; auto& e : v) {
  std::cout << e;
}
// prints "123"

[[likely]] and [[unlikely]] attributes

Provides a hint to the optimizer that the labelled statement has a high probability of being executed.

switch (n) {
case 1:
  // ...
  break;

[[likely]] case 2:  // n == 2 is considered to be arbitrarily more
  // ...            // likely than any other value of n
  break;
}

If one of the likely/unlikely attributes appears after the right parenthesis of an if-statement, it indicates that the branch is likely/unlikely to have its substatement (body) executed.

int random = get_random_number_between_x_and_y(0, 3);
if (random > 0) [[likely]] {
  // body of if statement
  // ...
}

It can also be applied to the substatement (body) of an iteration statement.

while (unlikely_truthy_condition) [[unlikely]] {
  // body of while statement
  // ...
}

Deprecate implicit capture of this

Implicitly capturing this in a lambda capture using [=] is now deprecated; prefer capturing explicitly using [=, this] or [=, *this].

struct int_value {
  int n = 0;
  auto getter_fn() {
    // BAD:
    // return [=]() { return n; };

    // GOOD:
    return [=, *this]() { return n; };
  }
};

Class types in non-type template parameters

Classes can now be used in non-type template parameters. Objects passed in as template arguments have the type const T, where T is the type of the object, and has static storage duration.

struct foo {
  foo() = default;
  constexpr foo(int) {}
};

template <foo f = {}>
auto get_foo() {
  return f;
}

get_foo(); // uses implicit constructor
get_foo<foo{123}>();

constexpr virtual functions

Virtual functions can now be constexpr and evaluated at compile-time. constexpr virtual functions can override non-constexpr virtual functions and vice-versa.

struct X1 {
  virtual int f() const = 0;
};

struct X2: public X1 {
  constexpr virtual int f() const { return 2; }
};

struct X3: public X2 {
  virtual int f() const { return 3; }
};

struct X4: public X3 {
  constexpr virtual int f() const { return 4; }
};

constexpr X4 x4;
x4.f(); // == 4

explicit(bool)

Conditionally select at compile-time whether a constructor is made explicit or not. explicit(true) is the same as specifying explicit.

struct foo {
  // Specify non-integral types (strings, floats, etc.) require explicit construction.
  template <typename T>
  explicit(!std::is_integral_v<T>) foo(T) {}
};

foo a = 123; // OK
foo b = "123"; // ERROR: explicit constructor is not a candidate (explicit specifier evaluates to true)
foo c {"123"}; // OK

Immediate functions

Similar to constexpr functions, but functions with a consteval specifier must produce a constant. These are called immediate functions.

consteval int sqr(int n) {
  return n * n;
}

constexpr int r = sqr(100); // OK
int x = 100;
int r2 = sqr(x); // ERROR: the value of 'x' is not usable in a constant expression
                 // OK if `sqr` were a `constexpr` function

using enum

Bring an enum's members into scope to improve readability. Before:

enum class rgba_color_channel { red, green, blue, alpha };

std::string_view to_string(rgba_color_channel channel) {
  switch (channel) {
    case rgba_color_channel::red:   return "red";
    case rgba_color_channel::green: return "green";
    case rgba_color_channel::blue:  return "blue";
    case rgba_color_channel::alpha: return "alpha";
  }
}

After:

enum class rgba_color_channel { red, green, blue, alpha };

std::string_view to_string(rgba_color_channel my_channel) {
  switch (my_channel) {
    using enum rgba_color_channel;
    case red:   return "red";
    case green: return "green";
    case blue:  return "blue";
    case alpha: return "alpha";
  }
}

Lambda capture of parameter pack

Capture parameter packs by value:

template <typename... Args>
auto f(Args&&... args){
    // BY VALUE:
    return [...args = std::forward<Args>(args)] {
        // ...
    };
}

Capture parameter packs by reference:

template <typename... Args>
auto f(Args&&... args){
    // BY REFERENCE:
    return [&...args = std::forward<Args>(args)] {
        // ...
    };
}

char8_t

Provides a standard type for representing UTF-8 strings.

char8_t utf8_str[] = u8"\u0123";

constinit

The constinit specifier requires that a variable must be initialized at compile-time.

const char* g() { return "dynamic initialization"; }
constexpr const char* f() { return "constant initializer"; }

constinit const char* c = f();  // OK
constinit const char* d = g();  // ERROR: `g` is not constexpr, so `d` cannot be evaluated at compile-time.

__VA_OPT__

Helps support variadic macros by evaluating to the given argument if the variadic macro is non-empty.

#define F(...) f(0 __VA_OPT__(,) __VA_ARGS__)
F(a, b, c) // replaced by f(0, a, b, c)
F()        // replaced by f(0)

C++20 Library Features

Text formatting

Provides a compile-time, checked string formatting library to the standard library using std::format. Text formatting can also be done at runtime for dynamic formatted strings using std::vformat and other help utilities. Text formatting follows the given specification.

std::format receives a format string as the first argument, and a variable number of arguments proceeding it. If formatting fails, the compilation will fail:

std::format("{}", 123); // OK -- returns "123"
std::format("{} {}", 123); // ERROR -- not enough arguments
std::format("{} {}", "Here's a number:", 123); // OK

Formatting a string based on a formatter created at runtime:

std::string fmt = "{} {}";
fmt += "{}{}";
std::vformat(fmt, std::make_format_args("Here's a number:", 1, 2, 3));
// OK -- returns "Here's a number: 123"

When formatting fails (such as an invalid format string), std::vformat will throw a std::format_error.

To format custom types:

struct fraction {
  int numerator;
  int denominator;
};

template <>
struct std::formatter<fraction> {
  constexpr auto parse(std::format_parse_context& ctx) {
    return ctx.begin();
  }

  auto format(const fraction& f, std::format_context& ctx) const {
    return std::format_to(ctx.out(), "{0:d}/{1:d}", f.numerator, f.denominator);
  }
};

fraction f{1, 2};
std::format("{}", f); // == "1/2"

Concepts library

Concepts are also provided by the standard library for building more complicated concepts. Some of these include:

Core language concepts:

  • same_as - specifies two types are the same.
  • derived_from - specifies that a type is derived from another type.
  • convertible_to - specifies that a type is implicitly convertible to another type.
  • common_with - specifies that two types share a common type.
  • integral - specifies that a type is an integral type.
  • default_constructible - specifies that an object of a type can be default-constructed.

Comparison concepts:

  • boolean - specifies that a type can be used in Boolean contexts.
  • equality_comparable - specifies that operator== is an equivalence relation.

Object concepts:

  • movable - specifies that an object of a type can be moved and swapped.
  • copyable - specifies that an object of a type can be copied, moved, and swapped.
  • semiregular - specifies that an object of a type can be copied, moved, swapped, and default constructed.
  • regular - specifies that a type is regular, that is, it is both semiregular and equality_comparable.

Callable concepts:

  • invocable - specifies that a callable type can be invoked with a given set of argument types.
  • predicate - specifies that a callable type is a Boolean predicate.

See also: concepts.

Synchronized buffered outputstream

Buffers output operations for the wrapped output stream ensuring synchronization (i.e. no interleaving of output).

std::osyncstream{std::cout} << "The value of x is:" << x << std::endl;

std::span

A span is a view (i.e. non-owning) of a container providing bounds-checked access to a contiguous group of elements. Since views do not own their elements they are cheap to construct and copy -- a simplified way to think about views is they are holding references to their data. As opposed to maintaining a pointer/iterator and length field, a span wraps both of those up in a single object.

Spans can be dynamically-sized or fixed-sized (known as their extent). Fixed-sized spans benefit from bounds-checking.

Span doesn't propagate const so to construct a read-only span use std::span<const T>.

Example: using a dynamically-sized span to print integers from various containers.

void print_ints(std::span<const int> ints) {
    for (const auto n : ints) {
        std::cout << n << std::endl;
    }
}

print_ints(std::vector{ 1, 2, 3 });
print_ints(std::array<int, 5>{ 1, 2, 3, 4, 5 });

int a[10] = { 0 };
print_ints(a);
// etc.

Example: a statically-sized span will fail to compile for containers that don't match the extent of the span.

void print_three_ints(std::span<const int, 3> ints) {
    for (const auto n : ints) {
        std::cout << n << std::endl;
    }
}

print_three_ints(std::vector{ 1, 2, 3 }); // ERROR
print_three_ints(std::array<int, 5>{ 1, 2, 3, 4, 5 }); // ERROR
int a[10] = { 0 };
print_three_ints(a); // ERROR

std::array<int, 3> b = { 1, 2, 3 };
print_three_ints(b); // OK

// You can construct a span manually if required:
std::vector c{ 1, 2, 3 };
print_three_ints(std::span<const int, 3>{ c.data(), 3 }); // OK: set pointer and length field.
print_three_ints(std::span<const int, 3>{ c.cbegin(), c.cend() }); // OK: use iterator pairs.

Bit operations

C++20 provides a new <bit> header which provides some bit operations including popcount.

std::popcount(0u); // 0
std::popcount(1u); // 1
std::popcount(0b1111'0000u); // 4

Math constants

Mathematical constants including PI, Euler's number, etc. defined in the <numbers> header.

std::numbers::pi; // 3.14159...
std::numbers::e; // 2.71828...

std::is_constant_evaluated

Predicate function which is truthy when it is called in a compile-time context.

constexpr bool is_compile_time() {
    return std::is_constant_evaluated();
}

constexpr bool a = is_compile_time(); // true
bool b = is_compile_time(); // false

std::make_shared supports arrays

auto p = std::make_shared<int[]>(5); // pointer to `int[5]`
// OR
auto p = std::make_shared<int[5]>(); // pointer to `int[5]`

starts_with and ends_with on strings

Strings (and string views) now have the starts_with and ends_with member functions to check if a string starts or ends with the given string.

std::string str = "foobar";
str.starts_with("foo"); // true
str.ends_with("baz"); // false

Check if associative container has element

Associative containers such as sets and maps have a contains member function, which can be used instead of the "find and check end of iterator" idiom.

std::map<int, char> map {{1, 'a'}, {2, 'b'}};
map.contains(2); // true
map.contains(123); // false

std::set<int> set {1, 2, 3};
set.contains(2); // true

std::bit_cast

A safer way to reinterpret an object from one type to another.

float f = 123.0;
int i = std::bit_cast<int>(f);

std::midpoint

Calculate the midpoint of two integers safely (without overflow).

std::midpoint(1, 3); // == 2

std::to_array

Converts the given array/"array-like" object to a std::array.

std::to_array("foo"); // returns `std::array<char, 4>`
std::to_array<int>({1, 2, 3}); // returns `std::array<int, 3>`

int a[] = {1, 2, 3};
std::to_array(a); // returns `std::array<int, 3>`

std::bind_front

Binds the first N arguments (where N is the number of arguments after the given function to std::bind_front) to a given free function, lambda, or member function.

const auto f = [](https://github.com/AnthonyCalandra/modern-cpp-features/blob/HEAD/int a, int b, int c) { return a + b + c; };
const auto g = std::bind_front(f, 1, 1);
g(1); // == 3

Uniform container erasure

Provides std::erase and/or std::erase_if for a variety of STL containers such as string, list, vector, map, etc.

For erasing by value use std::erase, or to specify a predicate when to erase elements use std::erase_if. Both functions return the number of erased elements.

std::vector v{0, 1, 0, 2, 0, 3};
std::erase(v, 0); // v == {1, 2, 3}
std::erase_if(v, [](https://github.com/AnthonyCalandra/modern-cpp-features/blob/HEAD/int n) { return n == 0; }); // v == {1, 2, 3}

Three-way comparison helpers

Helper functions for giving names to comparison results:

std::is_eq(0 <=> 0); // == true
std::is_lteq(0 <=> 1); // == true
std::is_gt(0 <=> 1); // == false

See also: three-way comparison.

std::lexicographical_compare_three_way

Lexicographically compares two ranges using three-way comparison and produces a result of the strongest applicable comparison category type.

std::vector a{0, 0, 0}, b{0, 0, 0}, c{1, 1, 1};

auto cmp_ab = std::lexicographical_compare_three_way(
    a.begin(), a.end(), b.begin(), b.end());
std::is_eq(cmp_ab); // == true

auto cmp_ac = std::lexicographical_compare_three_way(
    a.begin(), a.end(), c.begin(), c.end());
std::is_lt(cmp_ac); // == true

See also: three-way comparison, three-way comparison helpers.

std::jthread

A thread of execution (like std::thread) that joins on destruction and can be signaled to stop.

As opposed to std::thread where you need to check if a thread is joinable and then join on it, a std::jthread will automatically attempt to join through its destructor.

Unlike std::thread you can request it stop by calling std::jthread::request_stop or through the thread's stop_source:

std::jthread t{
    [](https://github.com/AnthonyCalandra/modern-cpp-features/blob/HEAD/std::stop_token stoken) {
        while (!stoken.stop_requested()) {
            std::this_thread::sleep_for(1s);
        }
    }
};

// Request stop from the thread object:
t.request_stop();
// OR, through the stop source:
std::stop_source stopSource = t.get_stop_source();
stopSource.request_stop();

A std::stop_token can be used to query the stop state of a thread.

Safe integral comparisons

Compare integers, including those of distinct types, without the dangers of integer conversion.

-1 > 0U; // == true
std::cmp_greater(-1, 0U); // == false

std::cmp_equal(0U, 0); // == true
std::cmp_less_equal(-1, 1U); // == true

std::in_range<unsigned>(-1); // == false
std::in_range<char>(999999); // == false

C++17 Language Features

Template argument deduction for class templates

Automatic template argument deduction much like how it's done for functions, but now including class constructors.

template <typename T = float>
struct MyContainer {
  T val;
  MyContainer() : val{} {}
  MyContainer(T val) : val{val} {}
  // ...
};
MyContainer c1 {1}; // OK MyContainer<int>
MyContainer c2; // OK MyContainer<float>

Declaring non-type template parameters with auto

Following the deduction rules of auto, while respecting the non-type template parameter list of allowable types[*], template arguments can be deduced from the types of its arguments:

template <auto... seq>
struct my_integer_sequence {
  // Implementation here ...
};

// Explicitly pass type `int` as template argument.
auto seq = std::integer_sequence<int, 0, 1, 2>();
// Type is deduced to be `int`.
auto seq2 = my_integer_sequence<0, 1, 2>();

* - For example, you cannot use a double as a template parameter type, which also makes this an invalid deduction using auto.

Folding expressions

A fold expression performs a fold of a template parameter pack over a binary operator.

  • An expression of the form (... op e) or (e op ...), where op is a fold-operator and e is an unexpanded parameter pack, are called unary folds.
  • An expression of the form (e1 op ... op e2), where op are fold-operators, is called a binary fold. Either e1 or e2 is an unexpanded parameter pack, but not both.
template <typename... Args>
bool logicalAnd(Args... args) {
    // Binary folding.
    return (true && ... && args);
}
bool b = true;
bool& b2 = b;
logicalAnd(b, b2, true); // == true
template <typename... Args>
auto sum(Args... args) {
    // Unary folding.
    return (... + args);
}
sum(1.0, 2.0f, 3); // == 6.0

New rules for auto deduction from braced-init-list

Changes to auto deduction when used with the uniform initialization syntax. Previously, auto x {3}; deduces a std::initializer_list<int>, which now deduces to int.

auto x1 {1, 2, 3}; // error: not a single element
auto x2 = {1, 2, 3}; // x2 is std::initializer_list<int>
auto x3 {3}; // x3 is int
auto x4 {3.0}; // x4 is double

constexpr lambda

Compile-time lambdas using constexpr.

auto identity = [](https://github.com/AnthonyCalandra/modern-cpp-features/blob/HEAD/int n) constexpr { return n; };
static_assert(identity(123) == 123);
constexpr auto add = [](https://github.com/AnthonyCalandra/modern-cpp-features/blob/HEAD/int x, int y) {
  auto L = [=] { return x; };
  auto R = [=] { return y; };
  return [=] { return L() + R(); };
};

static_assert(add(1, 2)() == 3);
constexpr int addOne(int n) {
  return [n] { return n + 1; }();
}

static_assert(addOne(1) == 2);

Lambda capture this by value

Capturing this in a lambda's environment was previously reference-only. An example of where this is problematic is asynchronous code using callbacks that require an object to be available, potentially past its lifetime. *this (C++17) will now make a copy of the current object, while this (C++11) continues to capture by reference.

struct MyObj {
  int value {123};
  auto getValueCopy() {
    return [*this] { return value; };
  }
  auto getValueRef() {
    return [this] { return value; };
  }
};
MyObj mo;
auto valueCopy = mo.getValueCopy();
auto valueRef = mo.getValueRef();
mo.value = 321;
valueCopy(); // 123
valueRef(); // 321

Inline variables

The inline specifier can be applied to variables as well as to functions. A variable declared inline has the same semantics as a function declared inline.

// Disassembly example using compiler explorer.
struct S { int x; };
inline S x1 = S{321}; // mov esi, dword ptr [x1]
                      // x1: .long 321

S x2 = S{123};        // mov eax, dword ptr [.L_ZZ4mainE2x2]
                      // mov dword ptr [rbp - 8], eax
                      // .L_ZZ4mainE2x2: .long 123

It can also be used to declare and define a static member variable, such that it does not need to be initialized in the source file.

struct S {
  S() : id{count++} {}
  ~S() { count--; }
  int id;
  static inline int count{0}; // declare and initialize count to 0 within the class
};

Nested namespaces

Using the namespace resolution operator to create nested namespace definitions.

namespace A {
  namespace B {
    namespace C {
      int i;
    }
  }
}

The code above can be written like this:

namespace A::B::C {
  int i;
}

Structured bindings

A proposal for de-structuring initialization, that would allow writing auto [ x, y, z ] = expr; where the type of expr was a tuple-like object, whose elements would be bound to the variables x, y, and z (which this construct declares). Tuple-like objects include std::tuple, std::pair, std::array, and aggregate structures.

using Coordinate = std::pair<int, int>;
Coordinate origin() {
  return Coordinate{0, 0};
}

const auto [ x, y ] = origin();
x; // == 0
y; // == 0
std::unordered_map<std::string, int> mapping {
  {"a", 1},
  {"b", 2},
  {"c", 3}
};

// Destructure by reference.
for (const auto& [key, value] : mapping) {
  // Do something with key and value
}

Selection statements with initializer

New versions of the if and switch statements which simplify common code patterns and help users keep scopes tight.

{
  std::lock_guard<std::mutex> lk(mx);
  if (v.empty()) v.push_back(val);
}
// vs.
if (std::lock_guard<std::mutex> lk(mx); v.empty()) {
  v.push_back(val);
}
Foo gadget(args);
switch (auto s = gadget.status()) {
  case OK: gadget.zip(); break;
  case Bad: throw BadFoo(s.message());
}
// vs.
switch (Foo gadget(args); auto s = gadget.status()) {
  case OK: gadget.zip(); break;
  case Bad: throw BadFoo(s.message());
}

constexpr if

Write code that is instantiated depending on a compile-time condition.

template <typename T>
constexpr bool isIntegral() {
  if constexpr (std::is_integral<T>::value) {
    return true;
  } else {
    return false;
  }
}
static_assert(isIntegral<int>() == true);
static_assert(isIntegral<char>() == true);
static_assert(isIntegral<double>() == false);
struct S {};
static_assert(isIntegral<S>() == false);

UTF-8 character literals

A character literal that begins with u8 is a character literal of type char. The value of a UTF-8 character literal is equal to its ISO 10646 code point value.

char x = u8'x';

Direct list initialization of enums

Enums can now be initialized using braced syntax.

enum byte : unsigned char {};
byte b {0}; // OK
byte c {-1}; // ERROR
byte d = byte{1}; // OK
byte e = byte{256}; // ERROR

[[fallthrough]], [[nodiscard]], [[maybe_unused]] attributes

C++17 introduces three new attributes: [[fallthrough]], [[nodiscard]] and [[maybe_unused]].

  • [[fallthrough]] indicates to the compiler that falling through in a switch statement is intended behavior. This attribute may only be used in a switch statement, and must be placed before the next case/default label.
switch (n) {
  case 1: 
    // ...
    [[fallthrough]];
  case 2:
    // ...
    break;
  case 3:
    // ...
    [[fallthrough]];
  default:
    // ...
}
  • [[nodiscard]] issues a warning when either a function or class has this attribute and its return value is discarded.
[[nodiscard]] bool do_something() {
  return is_success; // true for success, false for failure
}

do_something(); // warning: ignoring return value of 'bool do_something()',
                // declared with attribute 'nodiscard'
// Only issues a warning when `error_info` is returned by value.
struct [[nodiscard]] error_info {
  // ...
};

error_info do_something() {
  error_info ei;
  // ...
  return ei;
}

do_something(); // warning: ignoring returned value of type 'error_info',
                // declared with attribute 'nodiscard'
  • [[maybe_unused]] indicates to the compiler that a variable or parameter might be unused and is intended.
void my_callback(std::string msg, [[maybe_unused]] bool error) {
  // Don't care if `msg` is an error message, just log it.
  log(msg);
}

__has_include

__has_include (operand) operator may be used in #if and #elif expressions to check whether a header or source file (operand) is available for inclusion or not.

One use case of this would be using two libraries that work the same way, using the backup/experimental one if the preferred one is not found on the system.

#ifdef __has_include
#  if __has_include(<optional>)
#    include <optional>
#    define have_optional 1
#  elif __has_include(<experimental/optional>)
#    include <experimental/optional>
#    define have_optional 1
#    define experimental_optional
#  else
#    define have_optional 0
#  endif
#endif

It can also be used to include headers existing under different names or locations on various platforms, without knowing which platform the program is running on, OpenGL headers are a good example for this which are located in OpenGL\ directory on macOS and GL\ on other platforms.

#ifdef __has_include
#  if __has_include(<OpenGL/gl.h>)
#    include <OpenGL/gl.h>
#    include <OpenGL/glu.h>
#  elif __has_include(<GL/gl.h>)
#    include <GL/gl.h>
#    include <GL/glu.h>
#  else
#    error No suitable OpenGL headers found.
# endif
#endif

Class template argument deduction

Class template argument deduction (CTAD) allows the compiler to deduce template arguments from constructor arguments.

std::vector v{ 1, 2, 3 }; // deduces std::vector<int>

std::mutex mtx;
auto lck = std::lock_guard{ mtx }; // deduces to std::lock_guard<std::mutex>

auto p = new std::pair{ 1.0, 2.0 }; // deduces to std::pair<double, double>*

For user-defined types, deduction guides can be used to guide the compiler how to deduce template arguments if applicable:

template <typename T>
struct container {
  container(T t) {}

  template <typename Iter>
  container(Iter beg, Iter end);
};

// deduction guide
template <typename Iter>
container(Iter b, Iter e) -> container<typename std::iterator_traits<Iter>::value_type>;

container a{ 7 }; // OK: deduces container<int>

std::vector<double> v{ 1.0, 2.0, 3.0 };
auto b = container{ v.begin(), v.end() }; // OK: deduces container<double>

container c{ 5, 6 }; // ERROR: std::iterator_traits<int>::value_type is not a type

C++17 Library Features

std::variant

The class template std::variant represents a type-safe union. An instance of std::variant at any given time holds a value of one of its alternative types (it's also possible for it to be valueless).

std::variant<int, double> v{ 12 };
std::get<int>(v); // == 12
std::get<0>(v); // == 12
v = 12.0;
std::get<double>(v); // == 12.0
std::get<1>(v); // == 12.0

std::optional

The class template std::optional manages an optional contained value, i.e. a value that may or may not be present. A common use case for optional is the return value of a function that may fail.

std::optional<std::string> create(bool b) {
  if (b) {
    return "Godzilla";
  } else {
    return {};
  }
}

create(false).value_or("empty"); // == "empty"
create(true).value(); // == "Godzilla"
// optional-returning factory functions are usable as conditions of while and if
if (auto str = create(true)) {
  // ...
}

std::any

A type-safe container for single values of any type.

std::any x {5};
x.has_value() // == true
std::any_cast<int>(x) // == 5
std::any_cast<int&>(x) = 10;
std::any_cast<int>(x) // == 10

std::string_view

A non-owning reference to a string. Useful for providing an abstraction on top of strings (e.g. for parsing).

// Regular strings.
std::string_view cppstr {"foo"};
// Wide strings.
std::wstring_view wcstr_v {L"baz"};
// Character arrays.
char array[3] = {'b', 'a', 'r'};
std::string_view array_v(array, std::size(array));
std::string str {"   trim me"};
std::string_view v {str};
v.remove_prefix(std::min(v.find_first_not_of(" "), v.size()));
str; //  == "   trim me"
v; // == "trim me"

std::invoke

Invoke a Callable object with parameters. Examples of callable objects are std::function or lambdas; objects that can be called similarly to a regular function.

template <typename Callable>
class Proxy {
  Callable c_;

public:
  Proxy(Callable c) : c_{ std::move(c) } {}

  template <typename... Args>
  decltype(auto) operator()(Args&&... args) {
    // ...
    return std::invoke(c_, std::forward<Args>(args)...);
  }
};

const auto add = [](https://github.com/AnthonyCalandra/modern-cpp-features/blob/HEAD/int x, int y) { return x + y; };
Proxy p{ add };
p(1, 2); // == 3

std::apply

Invoke a Callable object with a tuple of arguments.

auto add = [](https://github.com/AnthonyCalandra/modern-cpp-features/blob/HEAD/int x, int y) {
  return x + y;
};
std::apply(add, std::make_tuple(1, 2)); // == 3

std::filesystem

The new std::filesystem library provides a standard way to manipulate files, directories, and paths in a filesystem.

Here, a big file is copied to a temporary path if there is available space:

const auto bigFilePath {"bigFileToCopy"};
if (std::filesystem::exists(bigFilePath)) {
  const auto bigFileSize {std::filesystem::file_size(bigFilePath)};
  std::filesystem::path tmpPath {"/tmp"};
  if (std::filesystem::space(tmpPath).available > bigFileSize) {
    std::filesystem::create_directory(tmpPath.append("example"));
    std::filesystem::copy_file(bigFilePath, tmpPath.append("newFile"));
  }
}

std::byte

The new std::byte type provides a standard way of representing data as a byte. Benefits of using std::byte over char or unsigned char is that it is not a character type, and is also not an arithmetic type; while the only operator overloads available are bitwise operations.

std::byte a {0};
std::byte b {0xFF};
int i = std::to_integer<int>(b); // 0xFF
std::byte c = a & b;
int j = std::to_integer<int>(c); // 0

Note that std::byte is simply an enum, and braced initialization of enums become possible thanks to direct-list-initialization of enums.

Splicing for maps and sets

Moving nodes and merging containers without the overhead of expensive copies, moves, or heap allocations/deallocations.

Moving elements from one map to another:

std::map<int, string> src {{1, "one"}, {2, "two"}, {3, "buckle my shoe"}};
std::map<int, string> dst {{3, "three"}};
dst.insert(src.extract(src.find(1))); // Cheap remove and insert of { 1, "one" } from `src` to `dst`.
dst.insert(src.extract(2)); // Cheap remove and insert of { 2, "two" } from `src` to `dst`.
// dst == { { 1, "one" }, { 2, "two" }, { 3, "three" } };

Inserting an entire set:

std::set<int> src {1, 3, 5};
std::set<int> dst {2, 4, 5};
dst.merge(src);
// src == { 5 }
// dst == { 1, 2, 3, 4, 5 }

Inserting elements which outlive the container:

auto elementFactory() {
  std::set<...> s;
  s.emplace(...);
  return s.extract(s.begin());
}
s2.insert(elementFactory());

Changing the key of a map element:

std::map<int, string> m {{1, "one"}, {2, "two"}, {3, "three"}};
auto e = m.extract(2);
e.key() = 4;
m.insert(std::move(e));
// m == { { 1, "one" }, { 3, "three" }, { 4, "two" } }

Parallel algorithms

Many of the STL algorithms, such as the copy, find and sort methods, started to support the parallel execution policies: seq, par and par_unseq which translate to "sequentially", "parallel" and "parallel unsequenced".

std::vector<int> longVector;
// Find element using parallel execution policy
auto result1 = std::find(std::execution::par, std::begin(longVector), std::end(longVector), 2);
// Sort elements using sequential execution policy
auto result2 = std::sort(std::execution::seq, std::begin(longVector), std::end(longVector));

std::sample

Samples n elements in the given sequence (without replacement) where every element has an equal chance of being selected.

const std::string ALLOWED_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
std::string guid;
// Sample 5 characters from ALLOWED_CHARS.
std::sample(ALLOWED_CHARS.begin(), ALLOWED_CHARS.end(), std::back_inserter(guid),
  5, std::mt19937{ std::random_device{}() });

std::cout << guid; // e.g. G1fW2

std::clamp

Clamp given value between a lower and upper bound.

std::clamp(42, -1, 1); // == 1
std::clamp(-42, -1, 1); // == -1
std::clamp(0, -1, 1); // == 0

// `std::clamp` also accepts a custom comparator:
std::clamp(0, -1, 1, std::less<>{}); // == 0

std::reduce

Fold over a given range of elements. Conceptually similar to std::accumulate, but std::reduce will perform the fold in parallel. Due to the fold being done in parallel, if you specify a binary operation, it is required to be associative and commutative. A given binary operation also should not change any element or invalidate any iterators within the given range.

The default binary operation is std::plus with an initial value of 0.

const std::array<int, 3> a{ 1, 2, 3 };
std::reduce(std::cbegin(a), std::cend(a)); // == 6
// Using a custom binary op:
std::reduce(std::cbegin(a), std::cend(a), 1, std::multiplies<>{}); // == 6

Additionally you can specify transformations for reducers:

std::transform_reduce(std::cbegin(a), std::cend(a), 0, std::plus<>{}, times_ten); // == 60

const std::array<int, 3> b{ 1, 2, 3 };
const auto product_times_ten = [](https://github.com/AnthonyCalandra/modern-cpp-features/blob/HEAD/const auto a, const auto b) { return a * b * 10; };

std::transform_reduce(std::cbegin(a), std::cend(a), std::cbegin(b), 0, std::plus<>{}, product_times_ten); // == 140

Prefix sum algorithms

Support for prefix sums (both inclusive and exclusive scans) along with transformations.

const std::array<int, 3> a{ 1, 2, 3 };

std::inclusive_scan(std::cbegin(a), std::cend(a),
    std::ostream_iterator<int>{ std::cout, " " }, std::plus<>{}); // 1 3 6

std::exclusive_scan(std::cbegin(a), std::cend(a),
    std::ostream_iterator<int>{ std::cout, " " }, 0, std::plus<>{}); // 0 1 3

const auto times_ten = [](https://github.com/AnthonyCalandra/modern-cpp-features/blob/HEAD/const auto n) { return n * 10; };

std::transform_inclusive_scan(std::cbegin(a), std::cend(a),
    std::ostream_iterator<int>{ std::cout, " " }, std::plus<>{}, times_ten); // 10 30 60

std::transform_exclusive_scan(std::cbegin(a), std::cend(a),
    std::ostream_iterator<int>{ std::cout, " " }, 0, std::plus<>{}, times_ten); // 0 10 30

GCD and LCM

Greatest common divisor (GCD) and least common multiple (LCM).

const int p = 9;
const int q = 3;
std::gcd(p, q); // == 3
std::lcm(p, q); // == 9

std::not_fn

Utility function that returns the negation of the result of the given function.

const std::ostream_iterator<int> ostream_it{ std::cout, " " };
const auto is_even = [](https://github.com/AnthonyCalandra/modern-cpp-features/blob/HEAD/const auto n) { return n % 2 == 0; };
std::vector<int> v{ 0, 1, 2, 3, 4 };

// Print all even numbers.
std::copy_if(std::cbegin(v), std::cend(v), ostream_it, is_even); // 0 2 4
// Print all odd (not even) numbers.
std::copy_if(std::cbegin(v), std::cend(v), ostream_it, std::not_fn(is_even)); // 1 3

String conversion to/from numbers

Convert integrals and floats to a string or vice-versa. Conversions are non-throwing, do not allocate, and are more secure than the equivalents from the C standard library.

Users are responsible for allocating enough storage required for std::to_chars, or the function will fail by setting the error code object in its return value.

These functions allow you to optionally pass a base (defaults to base-10) or a format specifier for floating type input.

  • std::to_chars returns a (non-const) char pointer which is one-past-the-end of the string that the function wrote to inside the given buffer, and an error code object.
  • std::from_chars returns a const char pointer which on success is equal to the end pointer passed to the function, and an error code object.

Both error code objects returned from these functions are equal to the default-initialized error code object on success.

Convert the number 123 to a std::string:

const int n = 123;

// Can use any container, string, array, etc.
std::string str;
str.resize(3); // hold enough storage for each digit of `n`

const auto [ ptr, ec ] = std::to_chars(str.data(), str.data() + str.size(), n);

if (ec == std::errc{}) { std::cout << str << std::endl; } // 123
else { /* handle failure */ }

Convert from a std::string with value "123" to an integer:

const std::string str{ "123" };
int n;

const auto [ ptr, ec ] = std::from_chars(str.data(), str.data() + str.size(), n);

if (ec == std::errc{}) { std::cout << n << std::endl; } // 123
else { /* handle failure */ }

Rounding functions for chrono durations and timepoints

This HTML preview is truncated for page performance. The canonical Markdown file contains the complete snapshot.

MARKDOWN METRICS
6748words
176headings
183links
202code blocks
MDRSS ASSESSMENT
Evidence0/100medium confidence
Why MDRSS assigned this score
  • Imported from the supplied mdrss-final-2026-08-04 content base.
  • Source URL is recorded as provenance.
  • Agent usefulness score: 73/100.
Evidence (1)

Discussion 0

Sign in to join the discussion.