Aller au contenu

C++20 : les quatre grandes nouveautés et plus encore

Découvrez les fonctionnalités transformatrices de C++20 : concepts, ranges, opérateur spaceship, std::format, initialiseurs désignés, std::span, et plus encore.

C++20 : un bond générationnel

C++20 est la mise à jour la plus importante de C++ depuis C++11. Il introduit quatre fonctionnalités majeures — les Concepts, les Ranges, les Coroutines et les Modules — qui changent chacune en profondeur la façon d'écrire du C++. Au-delà de ces « quatre grandes », C++20 apporte aussi l'opérateur spaceship, std::format, std::span, les initialiseurs désignés et de nombreuses améliorations de constexpr. Cette leçon couvre tout sauf les coroutines et les modules, qui ont chacun leur propre leçon.

Concepts : contraindre les templates

Les concepts remplacent SFINAE par des contraintes lisibles et composables sur les paramètres de template. Ils produisent des messages d'erreur clairs lorsque les contraintes ne sont pas satisfaites :

concepts.cpp
#include <iostream>
#include <concepts>
#include <string>
#include <vector>
#include <numeric>

// Define a custom concept
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

// Use concept in a requires clause
template<typename T>
    requires Numeric<T>
T add(T a, T b) {
    return a + b;
}

// Shorthand syntax — concept as type constraint
auto multiply(Numeric auto a, Numeric auto b) {
    return a * b;
}

// Concept with multiple requirements
template<typename Container>
concept Summable = requires(Container c) {
    { c.begin() } -> std::input_or_output_iterator;
    { c.end() } -> std::input_or_output_iterator;
    { c.size() } -> std::convertible_to<std::size_t>;
};

template<Summable Container>
auto sum_container(const Container& c) {
    using T = typename Container::value_type;
    return std::accumulate(c.begin(), c.end(), T{});
}

int main() {
    std::cout << add(3, 4) << "\n";           // 7
    std::cout << multiply(2.5, 4.0) << "\n";  // 10.0

    std::vector<int> v{1, 2, 3, 4, 5};
    std::cout << sum_container(v) << "\n";     // 15

    // add(std::string("a"), std::string("b"));  // Compile error!
    // Error message: constraints not satisfied — Numeric<std::string> is false

    return 0;
}

Ranges : des algorithmes composables

Les ranges apportent à la STL une approche fonctionnelle, sous forme de pipeline. Les vues sont paresseuses — elles n'allouent ni ne copient de données, et composent des transformations qui ne s'exécutent qu'au moment de l'itération :

ranges.cpp
#include <iostream>
#include <ranges>
#include <vector>
#include <string>
#include <algorithm>

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

    // Pipeline: filter even, square them, take first 3
    auto result = numbers
        | std::views::filter([](int n) { return n % 2 == 0; })
        | std::views::transform([](int n) { return n * n; })
        | std::views::take(3);

    for (int n : result) {
        std::cout << n << " ";  // 4 16 36
    }
    std::cout << "\n";

    // Range-based algorithms with projections
    struct Employee {
        std::string name;
        int salary;
    };

    std::vector<Employee> team{
        {"Alice", 90000}, {"Bob", 75000}, {"Carol", 120000}
    };

    // Sort by salary using a projection — no custom comparator needed
    std::ranges::sort(team, {}, &Employee::salary);
    for (const auto& e : team) {
        std::cout << e.name << ": " << e.salary << "\n";
    }

    // iota generates an infinite sequence
    for (int i : std::views::iota(1) | std::views::take(5)) {
        std::cout << i << " ";  // 1 2 3 4 5
    }
    std::cout << "\n";

    return 0;
}

Comparaison à trois voies et initialiseurs désignés

L'opérateur spaceship <=> génère les six opérateurs de comparaison à partir d'une seule déclaration. Les initialiseurs désignés importent du C l'initialisation des champs par leur nom, avec en plus la sûreté de typage du C++ :

spaceship.cpp
#include <iostream>
#include <compare>
#include <string>

struct Version {
    int major;
    int minor;
    int patch;

    // One operator generates ==, !=, <, >, <=, >=
    auto operator<=>(const Version&) const = default;
};

struct Config {
    std::string host = "localhost";
    int port = 8080;
    bool ssl = false;
    int max_connections = 100;
};

int main() {
    Version v1{2, 0, 0};
    Version v2{1, 9, 5};

    if (v1 > v2) std::cout << "v1 is newer\n";  // true
    if (v1 != v2) std::cout << "versions differ\n";

    // Strong ordering gives you all 6 operators for free
    auto cmp = v1 <=> v2;
    if (cmp > 0) std::cout << "v1 > v2\n";

    // Designated initializers — name the fields you're setting
    Config cfg{
        .host = "example.com",
        .port = 443,
        .ssl = true
        // .max_connections keeps its default value (100)
    };

    std::cout << cfg.host << ":" << cfg.port
              << " SSL=" << std::boolalpha << cfg.ssl
              << " max=" << cfg.max_connections << "\n";

    return 0;
}

std::format et std::span

std::format apporte au C++ un formatage de chaînes à la Python. std::span est une vue non propriétaire sur une séquence contiguë d'éléments, qui remplace les paires pointeur brut + taille :

format_span.cpp
#include <iostream>
#include <format>
#include <span>
#include <vector>
#include <array>

// std::span — works with any contiguous container
void print_values(std::span<const int> data) {
    for (int v : data) {
        std::cout << std::format("{:>5}", v);
    }
    std::cout << "\n";
}

int main() {
    // std::format — type-safe, positional formatting
    std::string msg = std::format("Hello, {}! You are {} years old.", "Alice", 30);
    std::cout << msg << "\n";

    // Format specifiers
    std::cout << std::format("Hex: {:#x}, Oct: {:#o}, Bin: {:#b}\n", 255, 255, 255);
    std::cout << std::format("Pi: {:.4f}\n", 3.14159265);
    std::cout << std::format("{:<15} {:>10}\n", "Name", "Score");
    std::cout << std::format("{:<15} {:>10}\n", "Alice", 95);

    // std::span — unifies arrays, vectors, and raw buffers
    int c_array[] = {1, 2, 3, 4, 5};
    std::vector<int> vec{6, 7, 8, 9, 10};
    std::array<int, 3> arr{11, 12, 13};

    print_values(c_array);  // works with C array
    print_values(vec);      // works with vector
    print_values(arr);      // works with std::array

    // Subspan — slice without copying
    std::span<int> full(vec);
    auto first_three = full.subspan(0, 3);
    print_values(first_three);  //  6  7  8

    return 0;
}

Améliorations de constexpr et chrono

C++20 étend massivement ce qui peut être constexpr : fonctions virtuelles, dynamic_cast, try-catch, et même std::vector et std::string peuvent désormais être utilisés dans des contextes constexpr. Davantage de calculs passent donc à la compilation, ce qui produit un code plus rapide à l'exécution.

La bibliothèque se dote d'un système complet de calendrier et de fuseaux horaires. Vous pouvez représenter des dates comme 2024y/January/15, faire de l'arithmétique calendaire et convertir entre fuseaux horaires — le tout avec sûreté de typage et sans bibliothèque externe.

Attributs : [[likely]] et [[unlikely]] indiquent au compilateur quelles branches sont les chemins les plus fréquents, ce qui permet une meilleure génération de code dans les sections critiques pour les performances.

Bonne pratique

Inutile d'utiliser tout C++20 d'un coup. Commencez par les fonctionnalités qui offrent le plus gros gain de productivité avec le moins de perturbations :

1. Opérateur spaceship — ajoutez auto operator<=>(const T&) const = default; à vos types dès maintenant
2. Concepts — utilisez les concepts standard (std::integral, std::ranges::range) avant d'écrire les vôtres
3. std::format — remplacez le formatage par printf et stringstream
4. Ranges — commencez par std::views::filter et std::views::transform dans le nouveau code
5. Initialiseurs désignés — utilisez-les pour les structs de configuration et les types d'options

Gardez les coroutines et les modules pour le moment où votre équipe et votre chaîne d'outils seront prêtes.

Points clés
  • Les concepts remplacent SFINAE par des contraintes de template lisibles et composables, avec des messages d'erreur clairs
  • Les ranges fournissent des pipelines paresseux et composables grâce à l'opérateur | — sans conteneurs temporaires
  • L'opérateur spaceship <=> génère les six opérateurs de comparaison à partir d'une seule déclaration par défaut
  • std::format apporte au C++ un formatage de chaînes sûr au niveau des types, à la Python
  • std::span est une vue non propriétaire sur de la mémoire contiguë, qui remplace les paires pointeur + taille
  • Les initialiseurs désignés ({.field = value}) rendent la construction des structs auto-documentée

Quiz — Testez vos connaissances

(20 XP)

1. Que fait `auto operator<=>(const MyType&) const = default;` ?

2. Quel est le principal avantage des vues de Ranges (comme `std::views::filter`) ?

3. Quel problème les concepts de C++20 résolvent-ils ?