C++23 et aperçu de C++26
Explorez std::expected, std::print, deducing this et les améliorations des ranges de C++23, et découvrez en avant-première les futures fonctionnalités de C++26 comme les contrats, la réflexion et le pattern matching.
L'évolution continue
C++23 est une version « de consolidation » qui comble les lacunes laissées par C++20 et ajoute des fonctionnalités très attendues. std::expected apporte une gestion d'erreurs à la Rust, std::print offre des sorties modernes, deducing this permet de puissants patrons de métaprogrammation, et les ranges gagnent des dizaines de nouvelles vues. Pendant ce temps, C++26 s'annonce comme une autre version majeure avec les contrats, la réflexion et le pattern matching — des fonctionnalités qui vont profondément changer notre façon d'écrire du C++ sûr et expressif.
std::expected : une gestion d'erreurs moderne
std::expected contient soit une valeur de succès de type T, soit une erreur de type E. Combiné aux opérations monadiques (.and_then(), .transform(), .or_else()), il permet une gestion d'erreurs composable sans exceptions :
#include <iostream>
#include <expected>
#include <string>
#include <charconv>
#include <system_error>
enum class ParseError {
empty_input,
invalid_format,
out_of_range
};
std::expected<int, ParseError> parse_int(std::string_view sv) {
if (sv.empty()) return std::unexpected(ParseError::empty_input);
int result{};
auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), result);
if (ec == std::errc::invalid_argument)
return std::unexpected(ParseError::invalid_format);
if (ec == std::errc::result_out_of_range)
return std::unexpected(ParseError::out_of_range);
if (ptr != sv.data() + sv.size())
return std::unexpected(ParseError::invalid_format);
return result;
}
std::expected<int, ParseError> double_if_positive(int val) {
if (val <= 0) return std::unexpected(ParseError::out_of_range);
return val * 2;
}
int main() {
// Direct usage
auto result = parse_int("42");
if (result) {
std::cout << "Parsed: " << *result << "\n";
} else {
std::cout << "Error code: " << static_cast<int>(result.error()) << "\n";
}
// Monadic chaining — like Rust's Result::and_then
auto chained = parse_int("21")
.and_then(double_if_positive) // int -> expected<int, E>
.transform([](int v) { return v + 1; }) // int -> int (wrapped)
.or_else([](ParseError e) -> std::expected<int, ParseError> {
std::cout << "Recovering from error\n";
return 0; // default value on error
});
std::cout << "Chained result: " << *chained << "\n"; // 43
return 0;
}std::print et std::println
std::print et std::println combinent std::format et l'écriture en sortie, et remplacent std::cout << dans la plupart des cas. Ils sont plus rapides, sûrs au niveau des types et produisent un code lisible :
#include <print>
#include <vector>
#include <string>
#include <ranges>
int main() {
// Basic printing — no more << chains!
std::println("Hello, {}!", "C++23");
std::print("No newline here. ");
std::println("But here.");
// Formatting works just like std::format
int x = 42;
double pi = 3.14159;
std::println("x = {}, pi = {:.2f}", x, pi);
std::println("Hex: {:#x}, Binary: {:#b}", 255, 255);
// Print to stderr
std::println(stderr, "Warning: something happened");
// Containers and ranges (C++23 formatter support)
std::vector<int> v{1, 2, 3, 4, 5};
std::println("Vector: {}", v); // [1, 2, 3, 4, 5]
// Formatted table
std::println("{:<15} {:>10} {:>10}", "Name", "Score", "Grade");
std::println("{:<15} {:>10} {:>10}", "Alice", 95, "A");
std::println("{:<15} {:>10} {:>10}", "Bob", 87, "B+");
return 0;
}Améliorations des ranges : zip, chunk, slide
C++23 ajoute de nouvelles vues de ranges puissantes qui couvrent des cas courants nécessitant auparavant des boucles manuelles ou des bibliothèques tierces :
#include <print>
#include <ranges>
#include <vector>
#include <string>
int main() {
std::vector<std::string> names{"Alice", "Bob", "Carol"};
std::vector<int> scores{95, 87, 92};
// zip — combine multiple ranges element-wise
for (auto [name, score] : std::views::zip(names, scores)) {
std::println("{}: {}", name, score);
}
std::vector<int> data{1, 2, 3, 4, 5, 6, 7, 8, 9};
// chunk — split into groups of N
for (auto chunk : data | std::views::chunk(3)) {
std::print("[ ");
for (int v : chunk) std::print("{} ", v);
std::println("]");
}
// [ 1 2 3 ] [ 4 5 6 ] [ 7 8 9 ]
// slide — sliding window of size N
for (auto window : data | std::views::slide(3)) {
std::print("( ");
for (int v : window) std::print("{} ", v);
std::println(")");
}
// ( 1 2 3 ) ( 2 3 4 ) ( 3 4 5 ) ... ( 7 8 9 )
// cartesian_product — all combinations
std::vector<char> suits{'H', 'D', 'C', 'S'};
std::vector<int> ranks{1, 2, 3};
for (auto [suit, rank] : std::views::cartesian_product(suits, ranks)) {
std::print("{}{} ", suit, rank);
}
std::println("");
// enumerate — index + value (finally!)
for (auto [i, name] : std::views::enumerate(names)) {
std::println("[{}] {}", i, name);
}
return 0;
}Deducing this
Le deducing this de C++23 (aussi appelé « paramètre objet explicite ») permet aux fonctions membres de recevoir this sous forme de paramètre explicite et déduit. Plus besoin de dupliquer les surcharges const/non-const, les lambdas récursives deviennent possibles et le CRTP se simplifie :
struct Widget {
std::string name;
// Avant C++23 : deux surcharges identiques
// const std::string& get_name() const { return name; }
// std::string& get_name() { return name; }
// C++23 : une seule fonction gère les deux cas
template<typename Self>
auto&& get_name(this Self&& self) {
return std::forward<Self>(self).name;
}
};Le paramètre this Self&& self déduit si l'objet est const ou non-const, lvalue ou rvalue — et le transmet correctement. Ce patron, appelé idiome « deducing this », peut réduire de moitié les ensembles de surcharges de fonctions membres.
Aperçu de C++26 : contrats, réflexion, pattern matching
C++26 devrait inclure plusieurs fonctionnalités transformatrices :
Contrats — pre, post et contract_assert permettent d'exprimer des préconditions, des postconditions et des assertions que le système de build peut activer ou désactiver :
int sqrt_int(int x)
pre(x >= 0)
post(r: r * r <= x)
{
// implémentation
}Réflexion statique — Inspecter les types, les membres et les énumérateurs à la compilation. Générer du code à partir des champs d'une struct, dériver automatiquement la sérialisation, et plus encore — le tout sans macros.
Pattern matching — Les expressions inspect permettent de filtrer selon les types, les valeurs et la structure :
inspect (variant_value) {
<int> i => std::println("int: {}", i);
<string> s => std::println("string: {}", s);
__ => std::println("other");
};Sender/Receiver (std::execution) — Un framework standard d'exécution asynchrone qui offre une concurrence structurée et remplace les implémentations de pools de threads faites maison.
Pour rester à jour avec l'évolution de C++ :
1. Visez un standard minimal pour votre projet (C++17 est une base solide pour la plupart du code de production aujourd'hui)
2. Utilisez les options du compilateur pour choisir le standard : -std=c++17, -std=c++20, -std=c++23
3. Vérifiez la prise en charge par les compilateurs sur cppreference.com/compiler_support — tous les compilateurs n'implémentent pas toutes les fonctionnalités
4. Utilisez les macros de test de fonctionnalités comme __cpp_concepts, __cpp_lib_expected pour utiliser conditionnellement les fonctionnalités plus récentes
5. Lisez les propositions (wg21.link/pXXXX) des fonctionnalités que vous comptez adopter
6. Ne courez pas après chaque nouveauté — adoptez ce qui rend votre base de code plus simple et plus correcte, pas seulement plus récente
Adopter trop tôt les fonctionnalités du dernier standard a des coûts réels :
Bugs de compilateur : les nouvelles fonctionnalités comportent souvent des bugs d'implémentation qui ne sont découverts qu'à l'usage en production. Les modules et les coroutines de C++20, par exemple, ont souffert de bugs de compilateur importants pendant 2 à 3 ans après leur standardisation.
Outillage incomplet : les débogueurs, profileurs et analyseurs statiques peuvent ne pas prendre entièrement en charge les fonctionnalités les plus récentes. La prise en charge par les IDE (IntelliSense, complétion de code) est souvent à la traîne.
Portabilité : si votre code doit compiler sur plusieurs plateformes (Linux, macOS, Windows, embarqué), vous êtes limité à l'intersection de ce que prennent en charge tous les compilateurs cibles.
Préparation de l'équipe : les nouvelles fonctionnalités exigent un apprentissage de toute l'équipe. Une fonctionnalité qu'un seul développeur comprend crée un goulot d'étranglement pour la maintenance.
std::expectedpermet une gestion d'erreurs à la Rust avec un chaînage monadique (.and_then(),.transform())std::print/std::printlnremplacentstd::cout <<par une sortie formatée claire, rapide et sûre au niveau des types- Les ranges de C++23 ajoutent les vues
zip,chunk,slide,cartesian_productetenumerate - Deducing this élimine la duplication des surcharges const/non-const des fonctions membres
- C++26 vise les contrats, la réflexion statique, le pattern matching et
std::executionpour l'asynchrone - Adoptez les nouveaux standards progressivement — vérifiez la prise en charge par les compilateurs, testez minutieusement et assurez-vous que l'équipe est prête
Quiz — Testez vos connaissances
(15 XP)1. Que représente `std::expected<int, Error>` ?
2. Que produit `std::views::zip(names, scores)` ?
3. Quelle fonctionnalité de C++26 permet de spécifier les préconditions et postconditions d'une fonction ?