Aller au contenu

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 :

structured_bindings.cpp
#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 :

vocabulary_types.cpp
#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 :

string_view_fs.cpp
#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 :

if_constexpr.cpp
#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;
}
Bonne pratique

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.

Piège

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).

Points clés
  • Les structured bindings (auto [a, b] = ...) décomposent proprement les tuples, pairs, structs et tableaux
  • std::optional remplace les valeurs sentinelles ; std::variant remplace les unions non sûres ; std::any sert pour les types réellement dynamiques
  • std::string_view évite les copies pour l'accès en lecture seule aux chaînes — mais attention aux références pendantes
  • if constexpr permet 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 ?