C++20/17/14/11
Overview
C++20 includes the following new language features:
- coroutines
- concepts
- designated initializers
- template syntax for lambdas
- range-based for loop with initializer
- [[likely]] and [[unlikely]] attributes
- deprecate implicit capture of this
- class types in non-type template parameters
- constexpr virtual functions
- explicit(bool)
- immediate functions
- using enum
- lambda capture of parameter pack
- char8_t
- constinit
C++20 includes the following new library features:
- concepts library
- synchronized buffered outputstream
- std::span
- bit operations
- math constants
- std::is_constant_evaluated
- std::make_shared supports arrays
- starts_with and ends_with on strings
- check if associative container has element
- std::bit_cast
- std::midpoint
- std::to_array
C++17 includes the following new language features:
- template argument deduction for class templates
- declaring non-type template parameters with auto
- folding expressions
- new rules for auto deduction from braced-init-list
- constexpr lambda
- lambda capture this by value
- inline variables
- nested namespaces
- structured bindings
- selection statements with initializer
- constexpr if
- utf-8 character literals
- direct-list-initialization of enums
- [[fallthrough]], [[nodiscard]], [[maybe_unused]] attributes
- __has_include
- class template argument deduction
C++17 includes the following new library features:
- std::variant
- std::optional
- std::any
- std::string_view
- std::invoke
- std::apply
- std::filesystem
- std::byte
- splicing for maps and sets
- parallel algorithms
- std::sample
- std::clamp
- std::reduce
- prefix sum algorithms
- gcd and lcm
- std::not_fn
- string conversion to/from numbers
C++14 includes the following new language features:
- binary literals
- generic lambda expressions
- lambda capture initializers
- return type deduction
- decltype(auto)
- relaxing constraints on constexpr functions
- variable templates
- [[deprecated]] attribute
C++14 includes the following new library features:
C++11 includes the following new language features:
- move semantics
- variadic templates
- rvalue references
- forwarding references
- initializer lists
- static assertions
- auto
- lambda expressions
- decltype
- type aliases
- nullptr
- strongly-typed enums
- attributes
- constexpr
- delegating constructors
- user-defined literals
- explicit virtual overrides
- final specifier
- default functions
- deleted functions
- range-based for loops
- special member functions for move semantics
- converting constructors
- explicit conversion functions
- inline-namespaces
- non-static data member initializers
- right angle brackets
- ref-qualified member functions
- trailing return types
- noexcept specifier
- char32_t and char16_t
- raw string literals
C++11 includes the following new library features:
- std::move
- std::forward
- std::thread
- std::to_string
- type traits
- smart pointers
- std::chrono
- tuples
- std::tie
- std::array
- unordered containers
- std::make_shared
- std::ref
- memory model
- std::async
- std::begin/end
C++20 Language Features
Coroutines
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
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.
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 = [](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.
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()