C++17 : le standard pragmatique
Explorez les fonctionnalités les plus marquantes de C++17 : structured bindings, std::optional/variant/any, std::string_view, std::filesystem, if constexpr et les nouveaux attributs.
Pourquoi C++17 est important
On qualifie souvent C++17 de « standard pragmatique », car il a introduit une foule de fonctionnalités qui simplifient le code de tous les jours sans exiger de connaissances poussées en métaprogrammation de templates. Il a supprimé des vestiges du passé (comme std::auto_ptr et les trigraphes), ajouté des types vocabulaire (std::optional, std::variant, std::any) et introduit du sucre syntaxique qui rend le code à la fois plus court et plus clair. Si vous écrivez du C++ en production aujourd'hui, C++17 devrait être votre base minimale.
Structured bindings et instructions d'initialisation
Les structured bindings permettent de décomposer des agrégats (structs, tableaux, pairs, tuples) en variables nommées dans une seule déclaration. Combinés aux initialiseurs de if/switch, ils éliminent de nombreuses variables temporaires :
#include <iostream>
#include <map>
#include <string>
#include <tuple>
std::tuple<std::string, int, double> get_employee() {
return {"Alice", 42, 95000.0};
}
int main() {
// Structured binding with tuple
auto [name, age, salary] = get_employee();
std::cout << name << " is " << age << " years old\n";
// Structured binding with map iteration
std::map<std::string, int> scores{{"Alice", 95}, {"Bob", 87}};
for (const auto& [student, score] : scores) {
std::cout << student << ": " << score << "\n";
}
// if with initializer — the variable is scoped to the if/else
if (auto it = scores.find("Alice"); it != scores.end()) {
std::cout << "Found: " << it->second << "\n";
} else {
std::cout << "Not found\n";
}
// 'it' no longer exists here — no scope leakage
// Structured binding with array
int arr[3] = {10, 20, 30};
auto [x, y, z] = arr;
std::cout << x << ", " << y << ", " << z << "\n";
return 0;
}Types vocabulaire : optional, variant, any
std::optional représente une valeur qui peut être présente ou non (en remplacement des valeurs sentinelles et des paramètres de sortie). std::variant est une union sûre au niveau des types. std::any contient n'importe quel type copiable, avec vérification du type à l'exécution :
#include <iostream>
#include <optional>
#include <variant>
#include <any>
#include <string>
// optional: replaces "return -1 on failure" patterns
std::optional<int> find_index(const std::string& haystack, char needle) {
for (size_t i = 0; i < haystack.size(); ++i) {
if (haystack[i] == needle) return static_cast<int>(i);
}
return std::nullopt; // no value
}
int main() {
// std::optional
auto idx = find_index("hello", 'l');
if (idx.has_value()) {
std::cout << "Found at index " << *idx << "\n"; // 2
}
std::cout << idx.value_or(-1) << "\n"; // safe default
// std::variant — type-safe union
std::variant<int, double, std::string> value = "hello";
std::cout << std::get<std::string>(value) << "\n";
// Visit pattern — exhaustive handling of all types
std::visit([](const auto& v) {
std::cout << "Value: " << v << "\n";
}, value);
value = 3.14; // now holds a double
std::cout << std::get<double>(value) << "\n";
// std::any — truly any type, with runtime checking
std::any data = 42;
std::cout << std::any_cast<int>(data) << "\n";
data = std::string("world");
std::cout << std::any_cast<std::string>(data) << "\n";
return 0;
}string_view et filesystem
std::string_view est une référence légère et non propriétaire vers une chaîne. Elle évite les copies lorsque vous avez seulement besoin de lire une chaîne. std::filesystem fournit une manipulation de chemins et des opérations sur les fichiers portables et multiplateformes :
#include <iostream>
#include <string>
#include <string_view>
#include <filesystem>
namespace fs = std::filesystem;
// string_view avoids copying — just a pointer + length
void print_trimmed(std::string_view sv) {
auto start = sv.find_first_not_of(' ');
auto end = sv.find_last_not_of(' ');
if (start != std::string_view::npos) {
std::cout << sv.substr(start, end - start + 1) << "\n";
}
}
int main() {
std::string name = " Hello, C++17! ";
print_trimmed(name); // no copy — string_view binds to string
print_trimmed(" world "); // no copy — binds to string literal
// std::filesystem — portable path operations
fs::path source_dir = "/home/user/project/src";
std::cout << "Filename: " << source_dir.filename() << "\n";
std::cout << "Parent: " << source_dir.parent_path() << "\n";
std::cout << "Exists: " << fs::exists(source_dir) << "\n";
// Iterate directory entries
for (const auto& entry : fs::directory_iterator(".")) {
if (entry.is_regular_file()) {
std::cout << entry.path().filename()
<< " (" << entry.file_size() << " bytes)\n";
}
}
// Create directories recursively
fs::create_directories("output/logs/2024");
return 0;
}if constexpr, CTAD et plus encore
if constexpr permet des branchements à la compilation à l'intérieur des templates — les branches qui ne correspondent pas sont entièrement écartées. La déduction d'arguments de template de classe (CTAD) vous permet d'omettre les arguments de template lorsque le compilateur peut les déduire. Les fold expressions simplifient la manipulation des packs de paramètres des templates variadiques :
#include <iostream>
#include <string>
#include <vector>
#include <type_traits>
// if constexpr — branches resolved at compile time
template<typename T>
std::string to_string_safe(T value) {
if constexpr (std::is_arithmetic_v<T>) {
return std::to_string(value);
} else if constexpr (std::is_same_v<T, std::string>) {
return value;
} else {
return "[unsupported type]";
}
}
// Fold expressions — collapse parameter packs
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // unary right fold
}
template<typename... Args>
void print_all(Args&&... args) {
((std::cout << args << " "), ...); // fold over comma
std::cout << "\n";
}
int main() {
std::cout << to_string_safe(42) << "\n";
std::cout << to_string_safe(std::string("hi")) << "\n";
std::cout << sum(1, 2, 3, 4, 5) << "\n"; // 15
print_all(1, "hello", 3.14); // 1 hello 3.14
// CTAD — no need for std::vector<int>
std::vector v{1, 2, 3, 4, 5}; // deduced as vector<int>
std::pair p{"hello", 42}; // deduced as pair<const char*, int>
// Nested namespaces
// namespace A::B::C { } instead of namespace A { namespace B { namespace C { } } }
// Inline variables — can be defined in headers without ODR violations
// inline constexpr int version = 17;
return 0;
}C++17 a standardisé trois attributs importants :
[[nodiscard]] — Émet un avertissement si une valeur de retour est ignorée. Utilisez-le sur les fonctions dont la valeur de retour doit toujours être vérifiée (codes d'erreur, ressources allouées, fonctions de fabrique).
[[maybe_unused]] — Supprime les avertissements de variable/paramètre inutilisé. Utile pour les variables employées uniquement dans les builds de débogage ou dans du code spécifique à une plateforme.
[[fallthrough]] — Indique qu'un passage au case suivant d'un switch est intentionnel, ce qui fait taire les avertissements du compilateur.
Adoptez [[nodiscard]] sans retenue dans vos API. Un code d'erreur ignoré est un bug qui ne demande qu'à se produire.
Ne retournez jamais un std::string_view qui référence une chaîne locale. Comme string_view ne possède pas ses données, la chaîne sous-jacente peut être détruite avant que la vue ne soit utilisée, ce qui provoque un comportement indéfini :
// DANGEREUX — retourne une vue sur un objet temporaire détruit !
std::string_view bad() {
std::string s = "hello";
return s; // s est détruite à la fin de la fonction !
}Règle empirique : utilisez string_view pour les paramètres (lecture de données), et retournez un std::string pour les valeurs de retour (données possédées).
- Les structured bindings (
auto [a, b] = ...) décomposent proprement les tuples, pairs, structs et tableaux std::optionalremplace les valeurs sentinelles ;std::variantremplace les unions non sûres ;std::anysert pour les types réellement dynamiquesstd::string_viewévite les copies pour l'accès en lecture seule aux chaînes — mais attention aux références pendantesif constexprpermet des branchements à la compilation dans les templates, en éliminant le code répétitif de SFINAE- La CTAD permet au compilateur de déduire les arguments de template :
std::vector v{1,2,3}fonctionne tout simplement - Utilisez
[[nodiscard]]sur les fonctions dont la valeur de retour ne doit pas être ignorée
Quiz — Testez vos connaissances
(15 XP)1. Que fait `auto [x, y] = std::make_pair(1, 2.0);` en C++17 ?
2. Que représente `std::optional<int>` ?
3. Pourquoi est-il dangereux de retourner depuis une fonction un `std::string_view` qui référence une chaîne locale ?