Xavier Lamorlette

C++ 20

Sommaire :

Syntaxe

consteval et constinit

consteval qualifie une fonction qui est évaluée exclusivement à la compilation (“immediate function”).

consteval int increment(int n) {
    return n + 1;
}

constinit qualifie une variable qui est initialisée à la compilation (mais qui n'est pas nécessairement constante).

constinit int a = increment(1);
a ++;

Constexpr Virtual Methods

On peut désormais utiliser des méthodes virtuelles dans des expressions constantes.

Designated Initialisers

class My_class {
public:
    int a = 0;
    int b = 0;
    int c = 0;
};

{
    My_class object{.a = 1, .c = 3};
}

Spaceship <=> operator

Comparaison trilatérale (“three-way comparison operator”) :

Cet opérateur doit être une relation d'ordre total (weak ordering), c'est-à-dire vérifiant (a == b || a < b || a > b) == true. == signifie ici que les éléments sont équivalents mais pas forcément identiques.

Avec une relation d'ordre strict total (strict weak ordering), == signifie que les éléments sont identiques.

Cet opérateur peut être construit automatiquement :

#include <compare>
class A {
    auto operator <=> (const A &) const = default;
};

Attributs likely et unlikely

if (condition) [[likely]] {
   …
} else [[unlikely]] {
   …
}

Attribut nodiscard

Indique que la valeur de retour ne doit pas être ignorée.

Boucle for avec initialisation

for (std::vector<A> collection = toto(); const A & item: collection) {…}
for (size_t index = 0; const A & item: collection) {
    …
    index++;
}

using enum

enum class Toto: uint32_t {
  First = 0
  Second
};
using enum Toto;
Toto toto = First;

Concepts

Les concepts permettent de définir sémantiquement les contraintes de validité des paramètres templates.
Ce sont des prédicats booléens, nommés, sur des paramètres templates, évalués à la compilation.

La surcharge avec des concepts permet de remplacer les traits et les enable_if.
Les concepts peuvent être utilisés pour la résolution de surcharge au lieu du SFINAE.

Exemples :

template <typename T>
concept Integral = std::is_integral<T>::value;

template <typename T>
concept Addable = requires(T a, T b) {
    a + b;
};

template <typename T>
concept EqualityComparable = requires(T a, T b) {
    {a == b} -> std::same_as<bool>;
    {a != b} -> std::same_as<bool>;
};

template <EqualityComparable T>
bool are_equal(const T & a,
        const T & b);
    return a == b;
}

Remarque : les concepts ne contenant qu'une seule propriété ou contrainte sont suspects.

Une expression requires teste si les paramètres templates fournissent les interfaces désirées, et renvoie un booléen :

template <typename Iterator>
struct Foo {
  static_assert(requires(Iterator i) {i ++;}, "template parameter does not provide increment");
}

Tester un type sur un concept :

static_assert(Concept<My_type>);

L'utilisation d'un concept dans la signature d'une fonction rend cette fonction template :

void foo(Integral a);
    // ...
}

Les concepts peuvent être utilisés au lieu du susbtitut non contraint auto :

Sortable s = f(x);  // au lieu de auto s = f(x);

requires peut être mis avant ou après le nom :

template <typename T>
requires Integral<T>()
T foo(T x) {
  …
}

template <typename T>
T foo(T x)
requires Integral<T>() {
  …
}

Les syntaxes abrégées suivantes sont équivalentes à celles ci-dessus :

template <Integral T>
T foo(T x) {
  …
}

Integral auto foo(Integral auto x) {
  …
}

Regular et SemiRegular

SemiRegular est un concept équivalent à DefaultConstructible + Destructible + CopyConstructible + CopyAssignable + MoveConstructible + MoveAssignable + Swappable.

Regular est un concept équivalent à SemiRegular + EqualityComparable.

Les containeurs et les algorithmes de la STL requièrent des types Regular.

Coroutines

Les coroutines sont des fonctions asynchrones, c'est-à-dire qui peuvent suspendre et reprendre leur exécution tout en conservant leur état.
Elles permettent de faire :

co_yield suspend l'exécution d'une fonction et retourne une valeur. Cela permet d'écrire des générateurs, avec évaluation retardée (appel par nécessité, “lazy evaluation”) :

std::generator<int> arithmetic_suite(int first_element,
        int difference) {
    for (int element = first_element; ; element += difference) {
        co_yield element;
    }
}

co_return permet de retourner une valeur depuis une coroutine.

co_await permet de suspendre et reprendre l'exécution en fonction d'une expression qui doit implémenter :

Modules

Les modules permettent de :

// module interface: my_module.ixx
module;

import std.core;

export module my_module;

export namespace my_module {
int foo(int arg);
}
// module implementation: my_module.cpp
module my_module;

import std.core;

namespace my_module {

int foo(int arg) {
  …
}

}
// client code
import std.core;

import my_module;

{
    my_module::foo(1);
}

Le module std.core contient quasiment toute la STL (notamment sauf std.memory).

On peut mettre l'interface et l'implémentation dans le même fichier comme suit :

// my_module.ixx
module;

import std.core;

export module my_module;

export namespace my_module {
int foo(int arg);
}

module :private;

int foo(int arg) {
  …
}

Ranges

La bibliothèque Ranges permet d'appliquer directement des algorithmes sur des collections, sans passer par des itérateurs, dans un style de programmation fonctionnelle.

std::range

Un std::range est un groupe d'éléments sur lequel on peut itérer.

Exemples :

const std::string_view value;
size_t length = std::ranges::distance(value);
std::ranges::count(value, 'x');
auto sub_string = std::ranges::subrange(std::ranges::find(value, 'x') + 1, std::ranges::end(value));
std::ranges::all_of(value, predicate);

std::views

Une vue est une adaptation composable d'un range, qui ne possède pas les données.
On compose les algorithmes grâce à l'opérateur pipe |.
La fonction d'adaptation est appliquée lorsque l'on itère sur la view, par un mécansime d'évaluation retardée (“lazy evaluation”).
Cela permet notamment d'appliquer les algorithmes sur des flux infinis de données (streams ou pipelines).
std::views est un alias de std::ranges::views.

Exemples :

std::map<…, …> a_map;
std::views::keys(a_map)
std::views::values(a_map)
std::views::reverse
std::views::take(12)
std::views::filter(predicate)
std::views::transform(function)

std::vector<int> numbers = {1, 2, 3, 4};
std::vector<int> incremented_odd_numbers = numbers
    | std::views::filter([](int n){return n % 2 == 1;})
    | std::views::transform([](int n){ return n + 1;});

auto result = collection
    | std::views::drop_while(predicate)
    | std::views::take_while(predicate_2);

STL

std::format

std::format permet de formatter les chaînes de caractères.
Cela remplace notamment l'utilisation de iomanip.

std::span

std::span est une vue sur une séquence contigue d'objets.
Il n'est pas propriétaire de la mémoire. La mémoire contigue peut-être, par exemple, celle d'un tableau C, d'un array on d'un vector.
Ce peut être vu comme une généralisation de std::string_view (avec la possibilité de modifier les éléments).

Ce peut être utile pour traiter en parallèle différentes parties d'une séquence.

Containeurs constexpr

std::string et std::vector peuvent être constexpr.

Ajouts à std::string

std::string::starts_with(), std::string::ends_with()

Ajout à std::stringstream

std::stringstream::view() renvoie une std::string_view.

stringstream::view()

contains

Les containeurs de la STL ont maintenant une méthode contains().

erase

Au lieu de l'idiome “erase - remove”, on a maintenant les fonctions std::erase(container, value) et erase_if(container, predicate).

source_location

source_location::current() renvoie un objet avec les membres file_name, line et function_name.

std::jthread

Thread avec join automatique en sortie de portée.

La dernière mise à jour de cette page date d'août 2024.

Le contenu de ce site est, en tant qu'œuvre originale de l'esprit, protégé par le droit d'auteur.
Pour tout commentaire, vous pouvez m'écrire à xavier.lamorlette@gmail.com.