Skip to content

C++ Aide-mémoire

Langage à usage général avec POO, génériques et contrôle de bas niveau.

01

Prise en main

Hello World

Chaque programme C++ commence dans main(). <iostream> fournit std::cout (sortie standard) et std::cin (entrée standard). Le préfixe std:: fait référence au namespace standard ; using namespace std; peut l'éviter mais est déconseillé dans les en-têtes car il pollue le namespace global.

cpp
#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

Variables & auto

auto déduit le type depuis l'initialiseur (C++11). Utilisez auto lorsque le type est évident ou verbeux (itérateurs). const rend une valeur immuable ; constexpr évalue à la compilation pour de vraies constantes intégrées dans le binaire.

cpp
int age = 30;
double pi = 3.14159;
char grade = 'A';
bool is_dev = true;
std::string name = "Alice";
auto x = 42;          // int
auto y = 3.14;        // double
const double TAX = 0.08;

Entrée & sortie

std::getline lit une ligne complète y compris les espaces, tandis que std::cin >> s'arrête au whitespace. Les mélanger laisse un newline dans le tampon ; appelez std::cin.ignore() entre getline et >> pour le supprimer.

cpp
#include <iostream>
#include <string>

int main() {
    std::string name;
    int age;
    std::cout << "Name: ";
    std::getline(std::cin, name);
    std::cout << "Age: ";
    std::cin >> age;
    std::cout << "Hi " << name << ", " << age << "\n";
}

Références

Les références sont des alias qui doivent être initialisés et ne peuvent pas être réaffectés à un autre objet. Le passage par référence évite la copie et permet de modifier la variable de l'appelant. Utilisez const T& pour les paramètres en lecture seule afin d'éviter les copies coûteuses.

cpp
int x = 10;
int &ref = x;   // alias for x
ref = 20;
std::cout << x;   // 20

void increment(int &n) { n++; }
int a = 5;
increment(a);   // a is now 6

Conversion de type

Préférez static_cast aux casts style C car il est explicite et vérifié à la compilation, rendant l'intention claire. std::stoi, std::stod convertissent les chaînes en nombres ; std::to_string fait l'inverse. Attention à std::out_of_range sur une mauvaise entrée.

cpp
double d = 3.99;
int i = (int)d;               // C-style: 3
int j = static_cast<int>(d);  // C++: 3

int n = 65;
char c = static_cast<char>(n);  // 'A'

// string <-> number
int num = std::stoi("42");
std::string s = std::to_string(3.14);
02

Chaînes

Bases de std::string

std::string gère sa propre mémoire et grandit selon les besoins. Contrairement aux tableaux char du C, vous ne gérez pas manuellement la longueur. .find() renvoie std::string::npos (une valeur énorme) lorsque la sous-chaîne n'est pas trouvée, donc comparez toujours à npos.

cpp
#include <string>
std::string s = "Hello";
std::cout << s.length();      // 5
std::cout << s[0];            // H
s += ", World!";
std::cout << s;               // Hello, World!
std::cout << s.substr(0, 3);  // Hel
std::cout << s.find("World"); // 7

Comparaison et recherche

La comparaison est lexicographique (ordre du dictionnaire). .find() cherche en avant, .rfind() cherche en arrière. Les deux renvoient std::string::npos si non trouvé, donc comparez toujours à npos plutôt que de traiter le résultat comme un booléen.

cpp
std::string a = "apple", b = "banana";
if (a == b) { /* equal */ }
if (a < b) { /* apple comes before banana */ }
if (a.find("pp") != std::string::npos) {
    std::cout << "found\n";
}
size_t pos = a.rfind("p");  // last occurrence

Stringstream

stringstream fait le pont entre les chaînes et les valeurs typées, utile pour construire des chaînes formatées (comme un tampon) ou analyser des jetons séparés par des espaces. C'est plus lent que les opérations directes mais très flexible pour la sérialisation et la désérialisation.

cpp
#include <sstream>
// Build a string
std::ostringstream oss;
oss << "Name=" << "Alice" << "&Age=" << 30;
std::string result = oss.str();

// Parse tokens
std::istringstream iss("10 20 30");
int a, b, c;
iss >> a >> b >> c;   // a=10, b=20, c=30

Chaînes brutes et multilignes

Les littéraux de chaîne brute R"(...)" traitent les barres obliques inverses et les guillemets littéralement, idéaux pour les patterns regex, les chemins de fichiers Windows et les modèles JSON/XML. Les délimiteurs à l'intérieur des parenthèses sont arbitraires, par ex. R"x(...)x" pour permettre ) à l'intérieur.

cpp
std::string raw = R"(C:\Users\name\file.txt)";
// No need to escape backslashes

std::string json = R"({
  "name": "Alice",
  "age": 30
})";

Tableaux char vs std::string

Les tableaux char style C nécessitent une gestion manuelle de la taille et sont sujets aux erreurs (débordements de tampon). Préférez std::string ; utilisez .c_str() lors de l'interface avec des APIs C qui attendent const char*. Notez que c_str() n'est valide que tant que la chaîne est vivante et non modifiée.

cpp
char cstr[] = "Hello";   // null-terminated, size 6
std::string cppstr = "Hello";

#include <cstring>
std::cout << strlen(cstr);      // 5
std::cout << cppstr.length();   // 5

std::string from_c = cstr;          // C -> C++
const char* to_c = cppstr.c_str();  // C++ -> C
03

Nombres & mathématiques

Types entiers et flottants

Utilisez les types à largeur fixe de <cstdint> (int32_t, int64_t) lorsque la taille exacte compte entre plateformes. Le séparateur de chiffres ' (C++14) améliore la lisibilité des grands nombres. double est le type flottant par défaut et est préféré à float pour la précision.

cpp
#include <cstdint>
int32_t a = 100;
int64_t big = 9'000'000'000LL;
uint8_t byte = 255;
size_t sz = sizeof(a);   // 4

float f = 1.5f;          // 4 bytes
double d = 3.14159265;   // 8 bytes (preferred)

Limites numériques

<limits> fournit des traits de type pour les propriétés numériques. Utilisez-les au lieu des macros INT_MAX codées en dur. epsilon() donne la plus petite différence distinguable par les flottants, utile pour comparer des doubles avec une tolérance.

cpp
#include <limits>
std::cout << std::numeric_limits<int>::max();    // 2147483647
std::cout << std::numeric_limits<int>::min();    // -2147483648
std::cout << std::numeric_limits<double>::infinity();
std::cout << std::numeric_limits<double>::epsilon();

Fonctions mathématiques

<cmath> fournit les fonctions mathématiques standard. Le débordement d'entier est un comportement indéfini en C++ ; utilisez int64_t ou vérifiez les bornes. Pour le code financier, rappelez-vous que les flottants sont imprécis — envisagez les centimes entiers ou une bibliothèque décimale.

cpp
#include <cmath>
double x = 2.5;
std::pow(x, 3);     // 15.625
std::sqrt(x);       // 1.581
std::abs(-5);       // 5
std::floor(3.7);    // 3.0
std::ceil(3.2);     // 4.0
std::round(3.5);    // 4.0
std::fmod(10.5, 3); // 1.5

Nombres aléatoires

Le C++ moderne utilise la bibliothèque <random> au lieu de rand(). mt19937 est un PRNG de haute qualité. Les distributions (uniform_int, uniform_real, normal) mappent les bits bruts vers la plage souhaitée sans le biais de modulo qui affecte rand() % N.

cpp
#include <random>
std::random_device rd;
std::mt19937 gen(rd());  // Mersenne Twister engine
std::uniform_int_distribution<int> dist(1, 100);

for (int i = 0; i < 5; i++) {
    std::cout << dist(gen) << " ";
}

Débordement d'entier et casts

Le débordement d'entier signé est un comportement indéfini en C++ (le compilateur peut optimiser en supposant qu'il n'arrive jamais). Convertissez toujours vers un type plus large avant de multiplier, ou vérifiez les bornes. Le débordement non signé s'enroule modulo 2^n et est bien défini.

cpp
int a = 100'000;
int b = a * a;   // overflow! undefined behavior
long long safe = (long long)a * a;  // OK

// Check before multiplying
if (a > 0 && b > INT_MAX / a) {
    // would overflow, handle it
}
04

Flux de contrôle

If / Else

C++17 introduit if avec initialiseur : if (auto it = m.find(k); it != m.end()) { ... }. Cela limite la variable au bloc if/else, gardant le périmètre environnant propre et évitant la réutilisation accidentelle.

cpp
int score = 85;
if (score >= 90) {
    std::cout << "A\n";
} else if (score >= 80) {
    std::cout << "B\n";
} else {
    std::cout << "C\n";
}

Switch

Incluez toujours break pour prévenir les retombées non intentionnelles. L'attribut [[fallthrough]] du C++17 documente les retombées intentionnelles pour silencer les avertissements. Switch fonctionne sur les types intégraux et enum, pas les chaînes ou les flottants.

cpp
int day = 3;
switch (day) {
    case 1: std::cout << "Mon"; break;
    case 2: std::cout << "Tue"; break;
    case 3: std::cout << "Wed"; break;
    default: std::cout << "Other";
}

Boucles for

Le for basé sur une plage itère les conteneurs proprement. Utilisez const auto& pour éviter de copier les éléments (important pour les chaînes et les grands objets). Pour modifier les éléments en place, utilisez auto& (référence non const).

cpp
// Classic for
for (int i = 0; i < 5; i++) {
    std::cout << i << " ";
}

// Range-based for (C++11)
std::vector<int> v = {1, 2, 3};
for (int n : v) std::cout << n;
for (const auto &n : v) std::cout << n;  // no copy

While & do-while

while vérifie la condition avant d'exécuter ; do-while exécute le corps au moins une fois avant de vérifier. do-while est utile pour la validation d'entrée et les boucles de menu où le corps doit s'exécuter avant que la condition puisse être évaluée.

cpp
int n = 5;
while (n > 0) {
    std::cout << n-- << " ";
}

int x;
do {
    std::cin >> x;
} while (x < 0);  // runs at least once

Break, continue et boucles imbriquées

break sort de la boucle englobante la plus proche ; continue passe à l'itération suivante. C++ n'a pas de break étiqueté comme Java ; utilisez une variable drapeau, ou extrayez la boucle dans une fonction et utilisez return pour sortir des boucles imbriquées.

cpp
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  // skip 3
    if (i == 7) break;     // stop at 7
    std::cout << i << " ";  // 0 1 2 4 5 6
}

// C++ has no labeled break; use a flag
bool found = false;
for (int i = 0; i < n && !found; i++) {
    for (int j = 0; j < m; j++) {
        if (matrix[i][j] == target) { found = true; break; }
    }
}
05

Fonctions & lambdas

Définir et retours multiples

Les liaisons structurées du C++17 (auto [a, b] = ...) dépaquettent les tuples, paires et structs proprement. Avant C++17, utilisez std::tie ou les paramètres de sortie. Le retour par valeur est bon marché grâce à la sémantique de déplacement (RVO) qui élude la copie.

cpp
#include <tuple>
std::tuple<int, int> divide(int a, int b) {
    return {a / b, a % b};
}

auto [q, r] = divide(17, 5);  // q=3, r=2 (C++17)

Default & inline

Les arguments par défaut permettent aux appelants d'omettre les paramètres finaux. inline est un indice au compilateur pour développer la fonction en ligne ; les compilateurs modernes décident eux-mêmes de l'inlining en fonction des drapeaux d'optimisation, donc inline concerne surtout l'ODR (règle de définition unique).

cpp
inline int power(int base, int exp = 2) {
    int r = 1;
    for (int i = 0; i < exp; i++) r *= base;
    return r;
}
// power(3) == 9, power(2, 5) == 32

Surcharge de fonctions

La surcharge permet aux fonctions de partager un nom mais de différer par les types de paramètres. Le compilateur choisit la meilleure correspondance via la résolution de surcharge. Les surcharges ambiguës causent des erreurs de compilation ; préférez les templates lorsque le corps est identique entre les types.

cpp
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
std::string add(std::string a, std::string b) { return a + b; }

add(1, 2);          // int version
add(1.5, 2.5);      // double version
add("a", "b");      // string version

Expressions lambda

Les lambdas créent des objets fonction anonymes en ligne. Les [] capturent les variables : [=] par valeur, [&] par référence, [x] spécifique par valeur, [&x] spécifique par référence. Ils sont essentiels pour les algorithmes STL et les rappels. Méfiez-vous des références pendantes lors de la capture par référence.

cpp
auto square = [](int x) { return x * x; };
std::cout << square(5);  // 25

int factor = 3;
auto multiply = [factor](int x) { return x * factor; };

std::vector<int> v = {1, 2, 3};
std::for_each(v.begin(), v.end(), [](int n) {
    std::cout << n << " ";
});

Pointeurs de fonction & std::function

std::function (de <functional>) contient tout appelable : fonctions, lambdas, functors. C'est plus flexible que les pointeurs de fonction bruts mais a un petit surcoût à l'exécution dû à l'effacement de type. Utilisez-le pour les rappels et le stockage d'appelables dans des conteneurs.

cpp
#include <functional>
int add(int a, int b) { return a + b; }

int (*fp)(int, int) = add;             // raw function pointer
std::function<int(int, int)> f = add;  // flexible wrapper
f = [](int a, int b) { return a * b; };

std::cout << f(3, 4);  // 12
06

Conteneurs STL

vector

vector est un tableau dynamique et le choix de conteneur par défaut. push_back est O(1) amorti. .at() fait la vérification des bornes (lance std::out_of_range), operator[] non. Appelez reserve() à l'avance si vous connaissez la taille pour éviter les réallocations.

cpp
#include <vector>
std::vector<int> v = {1, 2, 3};
v.push_back(4);
v.pop_back();
v.size();          // 3
v[0] = 0;
v.at(5);           // throws std::out_of_range
for (int n : v) std::cout << n;

map & unordered_map

map garde les clés triées (opérations O(log n)) ; unordered_map utilise le hachage (O(1) en moyenne). Utilisez map lorsque vous avez besoin d'une itération ordonnée ou de requêtes de plage ; unordered_map pour la vitesse de recherche pure. L'itération d'unordered_map n'est pas ordonnée.

cpp
#include <map>
#include <unordered_map>
std::map<std::string, int> ordered;   // sorted by key (red-black tree)
ordered["Alice"] = 30;

std::unordered_map<std::string, int> hashed;  // hash table
hashed["Bob"] = 25;

for (auto &[k, v] : ordered) {  // C++17 structured binding
    std::cout << k << ":" << v;
}

set & unordered_set

set stocke des éléments uniques triés (O(log n)). unordered_set est la version basée sur le hachage (O(1) en moyenne). Utilisez-les pour la déduplication et le test d'appartenance. lower_bound/upper_bound permettent les requêtes de plage sur les ensembles ordonnés.

cpp
#include <set>
std::set<int> s = {3, 1, 4, 1, 5};
// s contains: 1, 3, 4, 5 (sorted, unique)
s.insert(2);
s.erase(1);
if (s.count(4)) std::cout << "found";
auto it = s.lower_bound(3);  // first >= 3

array & deque

array est un tableau de taille fixe alloué sur la pile avec interface STL (plus sûr que les tableaux C, pas de dégradation en pointeur). deque (file à double extrémité) supporte push/pop en O(1) aux deux extrémités, contrairement à vector qui est en O(n) à l'avant.

cpp
#include <array>
#include <deque>
std::array<int, 3> arr = {1, 2, 3};  // fixed size, stack-allocated
arr.size();  // 3

std::deque<int> dq = {1, 2, 3};
dq.push_front(0);
dq.push_back(4);
// dq: 0, 1, 2, 3, 4

tuple & pair

tuple contient des valeurs hétérogènes de tout type. pair est un tuple à 2 éléments. Les liaisons structurées (C++17) les décomposent en variables nommées. Courant lors de l'itération de maps dont les éléments sont des paires de (clé, valeur).

cpp
#include <tuple>
std::tuple<int, std::string, double> t = {1, "Alice", 3.14};
auto [id, name, val] = t;  // C++17 structured binding

std::pair<int, int> p = {1, 2};
std::cout << p.first << p.second;

auto [a, b] = std::make_pair(10, 20);
07

Pointeurs & mémoire

Pointeurs bruts

Les pointeurs stockent des adresses mémoire. & obtient l'adresse, * déréférence. L'arithmétique des pointeurs fonctionne sur les tableaux. Les pointeurs bruts ne suivent pas la propriété, menant à des fuites et des pointeurs pendants — préférez les pointeurs intelligents pour les ressources possédées.

cpp
int x = 10;
int *ptr = &x;        // address of x
std::cout << *ptr;    // 10 (dereference)
*ptr = 20;
std::cout << x;       // 20

int arr[] = {1, 2, 3};
int *p = arr;
std::cout << *(p + 1);  // 2

Références vs pointeurs

Les références sont plus sûres (jamais nulles, toujours valides) et ont une syntaxe plus propre. Utilisez les références pour les paramètres de fonction et les valeurs de retour. Utilisez les pointeurs lorsque nul est un état significatif ou lorsque vous devez réaffecter ce qui est pointé.

cpp
int x = 10;
int &ref = x;    // must init, cannot reseat
int *ptr = &x;   // can be null, can reassign

ref = 20;        // x = 20
*ptr = 30;       // x = 30

// References cannot be null, safer for parameters
void foo(const std::string &s);  // preferred
void bar(std::string *s);        // s might be null

unique_ptr

unique_ptr est la propriété exclusive d'un objet tas. Il ne peut pas être copié, seulement déplacé. Se supprime automatiquement lorsqu'il sort du périmètre (RAII). C'est le pointeur intelligent par défaut pour la plupart des cas d'usage — surcoût nul par rapport à un pointeur brut.

cpp
#include <memory>
auto p = std::make_unique<int>(42);
std::cout << *p;  // 42
// auto p2 = p;   // ERROR: cannot copy
auto p2 = std::move(p);  // transfer ownership
// p is now nullptr

shared_ptr & weak_ptr

shared_ptr utilise le comptage de références ; l'objet est libéré lorsque le dernier shared_ptr est détruit. weak_ptr observe sans affecter le compteur, cassant les cycles de références. Évitez les cycles de shared_ptr (ils fuient car le compteur n'atteint jamais zéro).

cpp
#include <memory>
auto a = std::make_shared<int>(42);
auto b = a;  // both point to same object
std::cout << a.use_count();  // 2

std::weak_ptr<int> w = a;  // observer, no ownership
if (auto locked = w.lock()) {
    std::cout << *locked;  // 42
}

RAII & new/delete

RAII lie la durée de vie de la ressource au périmètre de l'objet : les constructeurs acquièrent, les destructeurs libèrent. Cela garantit le nettoyage même lorsque des exceptions se propagent. Préférez les vectors et les pointeurs intelligents au new/delete manuel — ils implémentent RAII pour vous.

cpp
// Manual new/delete (avoid in modern C++)
int *p = new int(42);
delete p;

// RAII: resource acquisition is initialization
class Buffer {
    int *data;
public:
    Buffer(size_t n) : data(new int[n]) {}
    ~Buffer() { delete[] data; }  // auto cleanup
};

// Modern: use containers/smart pointers instead
std::vector<int> buf(100);  // no manual delete needed
08

Classes & POO

Classe & constructeur

La liste d'initialisation des membres (: name(...), age(...)) initialise les membres avant que le corps ne s'exécute, plus efficace que l'affectation dans le corps. Marquez les getters const pour permettre l'appel sur des objets const. std::move évite de copier le paramètre chaîne.

cpp
class Person {
    std::string name;
    int age;
public:
    Person(std::string n, int a) : name(std::move(n)), age(a) {}
    std::string getName() const { return name; }
    int getAge() const { return age; }
};

Person p("Alice", 30);
std::cout << p.getName();

Modificateurs d'accès & encapsulation

Les membres private sont accessibles uniquement au sein de la classe ; protected permet les sous-classes ; public est ouvert à tous. L'encapsulation cache les détails d'implémentation, exposant une interface stable. Utilisez private par défaut et exposez uniquement ce qui est nécessaire.

cpp
class Account {
private:
    double balance;
protected:
    std::string owner;
public:
    Account(double b) : balance(b) {}
    double getBalance() const { return balance; }
    void deposit(double amt) { if (amt > 0) balance += amt; }
};

Héritage & virtual

virtual permet le polymorphisme à l'exécution — appeler speak() via un Animal* se distribue à la version de Dog. Déclarez toujours un destructeur virtuel dans les classes de base afin que la suppression via un pointeur de base appelle le destructeur dérivé. override attrape les fautes de frappe.

cpp
class Animal {
public:
    virtual void speak() { std::cout << "..."; }
    virtual ~Animal() = default;  // virtual destructor!
};

class Dog : public Animal {
public:
    void speak() override { std::cout << "Woof"; }
};

Animal *a = new Dog();
a->speak();  // Woof (polymorphism)
delete a;

Classes abstraites & interfaces

Une fonction virtuelle pure (= 0) rend la classe abstraite — vous ne pouvez pas l'instancier. Les classes avec uniquement des virtuelles pures agissent comme des interfaces Java. Les sous-classes concrètes doivent implémenter toutes les virtuelles pures ou elles restent abstraites.

cpp
class Shape {
public:
    virtual double area() const = 0;  // pure virtual
    virtual ~Shape() = default;
};

class Circle : public Shape {
    double r;
public:
    Circle(double r) : r(r) {}
    double area() const override {
        return 3.14159 * r * r;
    }
};

Surcharge d'opérateurs

La surcharge d'opérateurs permet aux types utilisateur de fonctionner avec +, <<, ==, etc. Surchargez uniquement lorsque la signification est intuitive (types mathématiques, itérateurs). L'opérateur << est couramment surchargé via friend pour la sortie de flux, permettant cout << myObject.

cpp
class Vec2 {
    double x, y;
public:
    Vec2(double x, double y) : x(x), y(y) {}
    Vec2 operator+(const Vec2 &o) const {
        return {x + o.x, y + o.y};
    }
    friend std::ostream &operator<<(std::ostream &os, const Vec2 &v) {
        return os << "(" << v.x << "," << v.y << ")";
    }
};

Vec2 a(1, 2), b(3, 4);
std::cout << a + b;  // (4,6)
09

Templates & génériques

Templates de fonction

Les templates de fonction génèrent des versions spécifiques au type à la compilation. Le compilateur déduit T depuis les arguments ; vous pouvez aussi le spécifier explicitement. Les templates sont des abstractions à coût nul — pas de surcoût à l'exécution, mais ils augmentent le temps de compilation et la taille du binaire.

cpp
template <typename T>
T max_val(T a, T b) {
    return (a > b) ? a : b;
}

std::cout << max_val(3, 7);           // int: 7
std::cout << max_val(3.14, 2.71);     // double: 3.14
std::cout << max_val<std::string>("a", "b");  // explicit

Templates de classe

Les templates de classe paramètrent des classes entières sur des types. Les conteneurs standard (vector, map) sont tous des templates. Le code template doit être dans les en-têtes (ou utilisez l'instanciation explicite) car le compilateur a besoin de la définition complète pour générer le code.

cpp
template <typename T>
class Stack {
    std::vector<T> data;
public:
    void push(T v) { data.push_back(v); }
    T pop() { T v = data.back(); data.pop_back(); return v; }
    bool empty() const { return data.empty(); }
};

Stack<int> si;
si.push(1); si.push(2);
Stack<std::string> ss;
ss.push("hi");

Spécialisation de template

La spécialisation complète fournit une implémentation personnalisée pour un type spécifique. La spécialisation partielle (uniquement pour les templates de classe) personnalise pour une catégorie de types (par ex., tous les types pointeur). Utile pour optimiser ou spécialiser un comportement.

cpp
template <typename T>
T identity(T x) { return x; }

// Full specialization for bool
template <>
bool identity<bool>(bool x) {
    std::cout << "bool!";
    return x;
}

identity(42);     // generic
identity(true);   // specialized

Templates variadiques

Les templates variadiques acceptent un nombre quelconque d'arguments via les parameter packs (...). Ils récursent pour traiter chaque argument. Les fold expressions du C++17 simplifient cela : (std::cout << ... << args). Largement utilisés dans std::make_shared, std::tuple.

cpp
template <typename T>
void print(T v) { std::cout << v << "\n"; }

template <typename T, typename... Args>
void print(T first, Args... rest) {
    std::cout << first << ", ";
    print(rest...);  // recurse
}

print(1, "hello", 3.14);  // 1, hello, 3.14

Concepts (C++20)

Les concepts (C++20) contraignent les paramètres de template avec des exigences lisibles, remplaçant les enable_if/SFINAE obscurs. Ils produisent des messages d'erreur beaucoup plus clairs lorsque les contraintes ne sont pas respectées. Utilisez les concepts standard comme std::integral, std::floating_point, std::convertible_to.

cpp
#include <concepts>
template <std::integral T>
T add(T a, T b) { return a + b; }

template <typename T>
requires std::floating_point<T>
T half(T x) { return x / 2; }

// Abbreviated form
auto square(std::integral auto x) { return x * x; }
10

Algorithmes STL

sort & find

Les algorithmes STL opèrent sur des plages d'itérateurs [begin, end). sort est O(n log n). find est linéaire ; pour les plages triées utilisez binary_search/lower_bound (O(log n)). Passez des comparateurs personnalisés (lambdas ou std::greater) pour un ordre personnalisé.

cpp
#include <algorithm>
std::vector<int> v = {3, 1, 4, 1, 5, 9};
std::sort(v.begin(), v.end());          // 1 1 3 4 5 9
std::sort(v.begin(), v.end(), std::greater<>());  // descending

auto it = std::find(v.begin(), v.end(), 4);
if (it != v.end()) std::cout << "found";

transform & for_each

transform mappe chaque élément à une nouvelle valeur (comme map dans les langages fonctionnels). for_each applique une fonction pour les effets de bord. Les ranges du C++20 permettent v | views::transform(...) pour un style de pipeline plus propre sans itérateurs begin/end.

cpp
std::vector<int> v = {1, 2, 3, 4};
std::vector<int> squared(v.size());
std::transform(v.begin(), v.end(), squared.begin(),
               [](int x) { return x * x; });
// squared: 1 4 9 16

std::for_each(v.begin(), v.end(), [](int &x) { x *= 2; });
// v: 2 4 6 8

accumulate & count

accumulate (de <numeric>) replie une plage avec une opération. Le troisième argument est la valeur initiale et détermine le type du résultat — utilisez 0.0 pour les sommes en double. count renvoie combien d'éléments égalent une valeur ; count_if utilise un prédicat.

cpp
#include <numeric>
std::vector<int> v = {1, 2, 3, 4, 5};
int sum = std::accumulate(v.begin(), v.end(), 0);  // 15
int product = std::accumulate(v.begin(), v.end(), 1,
                              std::multiplies<>());  // 120

int cnt = std::count(v.begin(), v.end(), 3);  // 1

copy, remove & unique

remove n'efface pas réellement — il déplace les éléments non correspondants vers l'avant et renvoie un nouvel itérateur de fin. Associez-le à .erase() pour l'idiome erase-remove. unique compacte de même les doublons consécutifs ; triez d'abord pour dédupliquer complètement.

cpp
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> dest;
std::copy(v.begin(), v.end(), std::back_inserter(dest));

auto end = std::remove(v.begin(), v.end(), 3);  // erase-remove idiom
v.erase(end, v.end());

std::vector<int> u = {1, 1, 2, 2, 3};
u.erase(std::unique(u.begin(), u.end()), u.end());  // 1 2 3

min, max & clamp

min/max renvoient le plus petit/plus grand de deux valeurs ou d'une initializer list. minmax renvoie les deux sous forme de paire. clamp (C++17) restreint une valeur à une plage, remplaçant les vérifications manuelles if/else — utile pour la validation d'entrée et les coordonnées UI.

cpp
int a = 3, b = 7;
std::cout << std::max(a, b);   // 7
std::cout << std::min(a, b);   // 3

auto [mn, mx] = std::minmax({3, 1, 4, 1, 5});  // mn=1, mx=5

int score = 105;
int clamped = std::clamp(score, 0, 100);  // 100
11

Gestion des erreurs

Exceptions : try/catch

Lancez les exceptions par valeur, attrapez par référence const pour éviter le slicing. Attraper std::exception attrape toutes les exceptions standard via la classe de base. Les exceptions sont pour des cas exceptionnels, pas pour le flux de contrôle normal — elles ont un surcoût lorsqu'elles sont lancées.

cpp
#include <stdexcept>
try {
    int x = 10, y = 0;
    if (y == 0) throw std::runtime_error("division by zero");
    std::cout << x / y;
} catch (const std::exception &e) {
    std::cerr << "Error: " << e.what();
}

Hiérarchie d'exceptions standard

<stdexcept> fournit les types d'exception standard. logic_error est pour les erreurs de programmeur (détectables avant l'exécution) ; runtime_error pour les conditions d'exécution imprévues. Dérivez les exceptions personnalisées de std::runtime_error afin qu'elles s'intègrent aux blocs catch standard.

cpp
#include <stdexcept>
throw std::runtime_error("runtime error");
throw std::logic_error("logic error");
throw std::out_of_range("index out of range");
throw std::invalid_argument("bad argument");
throw std::bad_alloc();  // thrown by new on allocation failure

// std::exception is the base; .what() returns the message

Exceptions personnalisées

Dérivez les exceptions personnalisées d'une base standard afin qu'elles s'intègrent à catch(const std::exception&). Ajoutez des champs de contexte (chemins de fichiers, codes d'erreur) qui aident au débogage. Passez toujours le message au constructeur de base afin que what() fonctionne.

cpp
class FileError : public std::runtime_error {
public:
    FileError(const std::string &msg, const std::string &path)
        : std::runtime_error(msg + ": " + path), path_(path) {}
    const std::string &path() const { return path_; }
private:
    std::string path_;
};

try { throw FileError("not found", "data.txt"); }
catch (const FileError &e) {
    std::cerr << e.what() << " at " << e.path();
}

noexcept & sécurité RAII

noexcept promet qu'une fonction ne lancera pas, permettant les optimisations du compilateur. Si elle lance quand même, std::terminate est appelé. RAII garantit que les destructeurs s'exécutent pendant le déroulement de la pile, donc les ressources sont libérées même lorsque des exceptions se propagent vers le haut de la pile d'appels.

cpp
void safe_op() noexcept {
    // guaranteed not to throw
}

void risky() {
    std::vector<int> v(1000);  // RAII: cleanup on exception
    throw std::runtime_error("oops");
    // v's destructor still runs during stack unwinding
}

Assertions

assert() vérifie des conditions dans les builds de débogage ; il est supprimé en release (lorsque NDEBUG est défini), donc ne l'utilisez pas pour des vérifications de production. Utilisez-le pour les invariants internes qui indiquent des bugs. Pour la validation orientée utilisateur, lancez des exceptions ou renvoyez des codes d'erreur.

cpp
#include <cassert>
double sqrt_safe(double x) {
    assert(x >= 0 && "sqrt of negative");
    return std::sqrt(x);
}
// In release builds (NDEBUG defined), assert is removed
12

E/S fichiers & flux

Lire un fichier

ifstream ouvre un fichier en lecture. Vérifiez toujours si l'ouverture a réussi (!file s'évalue à vrai en cas d'échec). getline lit ligne par ligne y compris les espaces. Le destructeur du flux ferme le fichier automatiquement (RAII), donc pas de fermeture manuelle nécessaire.

cpp
#include <fstream>
#include <string>
std::ifstream file("input.txt");
if (!file) { std::cerr << "cannot open"; return 1; }

std::string line;
while (std::getline(file, line)) {
    std::cout << line << "\n";
}

Écrire un fichier

ofstream écrit dans un fichier, en tronquant par défaut. Utilisez std::ios::app pour ajouter, std::ios::binary pour le mode binaire. L'opérateur << fonctionne exactement comme std::cout. Videz avec out.flush() ou utilisez std::endl (qui vide aussi).

cpp
#include <fstream>
std::ofstream out("output.txt");
if (!out) return 1;

out << "Line 1\n";
out << "Value: " << 42 << "\n";

out.close();  // optional, destructor closes
// Append mode: std::ofstream("f.txt", std::ios::app);

Flux de chaînes

ostringstream construit des chaînes à partir de types mixtes (comme un tampon). istringstream analyse les chaînes en valeurs typées. Ils sont plus lents que les opérations directes sur les chaînes mais très pratiques pour la sérialisation, la construction d'URL et l'analyse de jetons.

cpp
#include <sstream>
// Build a string from mixed types
std::ostringstream oss;
oss << "Name=" << "Alice" << "&Age=" << 30;
std::string url = oss.str();

// Parse tokens from a string
std::istringstream iss("10 20 30");
int a, b, c;
iss >> a >> b >> c;

Fichiers binaires

Le mode binaire évite la traduction des newlines et est plus compact que le texte. write/read prennent char* et le comptage d'octets — utilisez reinterpret_cast pour les structs. Note : les fichiers binaires ne sont pas portables entre architectures (endianness, remplissage de struct diffèrent).

cpp
#include <fstream>
struct Record { int id; double value; };

// Write
std::ofstream out("data.bin", std::ios::binary);
Record r{1, 3.14};
out.write(reinterpret_cast<char*>(&r), sizeof(r));

// Read
std::ifstream in("data.bin", std::ios::binary);
Record r2;
in.read(reinterpret_cast<char*>(&r2), sizeof(r2));

Sortie formatée (fmt C++20)

std::format (C++20) apporte les chaînes de format style Python à C++, remplaçant les manipulateurs iomanip désordonnés. Pour le code plus ancien, <iomanip> fournit setprecision, setw, setfill. La bibliothèque {fmt} est une alternative pré-C++20 populaire avec la même syntaxe.

cpp
#include <format>  // C++20
std::string s = std::format("Hello, {}! You are {}.", "Alice", 30);
std::cout << std::format("{:.2f}", 3.14159);  // 3.14
std::cout << std::format("{:>10}", "right");   // padded

// Pre-C++20: use iomanip
#include <iomanip>
std::cout << std::fixed << std::setprecision(2) << 3.14159;
13

Pointeurs intelligents

unique_ptr - Propriété exclusive

unique_ptr est le pointeur intelligent par défaut — utilisez-le lorsqu'un seul propriétaire suffit. Il a un surcoût nul par rapport aux pointeurs bruts. make_unique est préféré (sûr vis-à-vis des exceptions). Ne peut pas être copié, seulement déplacé. Les deleteurs personnalisés permettent RAII pour des ressources C comme FILE* ou les sockets.

cpp
#include <memory>
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1;  // ERROR: cannot copy
std::unique_ptr<int> p2 = std::move(p1);  // OK: transfer ownership
// p1 is now nullptr

// Custom deleter
auto deleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(deleter)> fp(fopen("f.txt", "r"), deleter);

shared_ptr - Propriété partagée

shared_ptr utilise le comptage de références — plusieurs pointeurs peuvent posséder le même objet. L'objet est détruit lorsque le refcount atteint 0. make_shared est préféré (allocation unique pour objet + bloc de contrôle). Plus lourd que unique_ptr en raison du refcount atomique et du bloc de contrôle. Utilisez-le lorsque la propriété est réellement partagée.

cpp
#include <memory>
auto p1 = std::make_shared<int>(42);
auto p2 = p1;  // OK: both share ownership
std::cout << *p1 << " " << p1.use_count();  // 42 2

// Control block holds refcount + deleter + allocator
// Refcount is atomic (thread-safe), but object access is NOT
std::shared_ptr<int> p3{new int{10}};  // uses non-array new

// Thread-safe: refcount operations are atomic
// NOT thread-safe: accessing the pointed-to object

weak_ptr - Casser les cycles

weak_ptr est un observateur non propriétaire d'un shared_ptr. N'augmente pas le refcount. Utilisez lock() pour obtenir temporairement un shared_ptr (renvoie null si l'objet a été détruit). Essentiel pour casser les cycles de références (par ex., listes doublement chaînées, relations parent-enfant) qui causeraient des fuites de mémoire.

cpp
#include <memory>
struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // weak to avoid cycle
};

auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a;  // weak_ptr from shared_ptr

// To use a weak_ptr, lock() into a shared_ptr
if (auto locked = b->prev.lock()) {
    std::cout << "prev exists";
} else {
    std::cout << "prev expired";
}

Pointeurs intelligents avec tableaux

Les pointeurs intelligents peuvent gérer des tableaux. unique_ptr<T[]> fournit operator[] et le delete[] correct. shared_ptr<T[]> est supporté depuis C++17. Cependant, std::vector ou std::array sont presque toujours meilleurs — plus sûrs, plus ergonomiques et auto-documentés. Utilisez les pointeurs intelligents de tableau uniquement lors de l'interface avec des APIs héritées.

cpp
#include <memory>
// C++17: shared_ptr supports arrays
std::shared_ptr<int[]> arr1(new int[10]);
arr1[0] = 42;

// unique_ptr with arrays (partial specialization exists)
std::unique_ptr<int[]> arr2 = std::make_unique<int[]>(10);
arr2[5] = 100;

// Prefer std::array or std::vector over raw arrays
#include <vector>
std::vector<int> v(10);  // better choice

enable_shared_from_this

Lorsqu'un objet doit renvoyer un shared_ptr vers lui-même, enable_shared_from_this fournit un shared_from_this() sûr. Appeler shared_ptr<T>(this) directement créerait un second bloc de contrôle, menant à un double-free. L'objet doit déjà être géré par un shared_ptr, sinon shared_from_this() lance bad_weak_ptr.

cpp
#include <memory>
class Widget : public std::enable_shared_from_this<Widget> {
public:
    std::shared_ptr<Widget> getPtr() {
        return shared_from_this();  // safe
        // return shared_ptr<Widget>(this);  // BAD: double delete
    }
};

auto w = std::make_shared<Widget>();
auto w2 = w->getPtr();  // shares ownership, refcount = 2
14

Sémantique de déplacement & références rvalue

Lvalues, rvalues et références

Les lvalues ont une identité et persistent au-delà d'une seule expression (objets nommés). Les rvalues sont des valeurs temporaires ou littérales. T& se lie aux lvalues, T&& se lie aux rvalues. const T& est spécial — il se lie aux deux. Comprendre cette distinction est le fondement de la sémantique de déplacement.

cpp
int x = 10;          // x is an lvalue
int& lref = x;       // lvalue reference
int&& rref = 20;     // rvalue reference (binds to temporary)
// int& bad = 20;    // ERROR: can't bind lref to rvalue
const int& cref = 20;  // OK: const lref binds to rvalue

int y = x + 5;       // (x + 5) is an rvalue (prvalue)
std::string s1 = "hi";
std::string&& rr = std::move(s1);  // rr is named rvalue

std::move et constructeurs de déplacement

std::move ne déplace rien — il convertit en rvalue, permettant au constructeur/affectation de déplacement d'être sélectionné. Les opérations de déplacement devraient être noexcept afin que les conteneurs puissent les utiliser lors de la réallocation (sinon ils retombent sur la copie pour la sécurité d'exception). Après un déplacement, l'objet source est dans un état valide mais non spécifié.

cpp
class Buffer {
    int* data;
    size_t size;
public:
    // Move constructor: steal resources
    Buffer(Buffer&& other) noexcept
        : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }
    // Move assignment
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data; size = other.size;
            other.data = nullptr; other.size = 0;
        }
        return *this;
    }
};

Buffer a(100);
Buffer b = std::move(a);  // calls move ctor, a is now empty

Transfert parfait

Le transfert parfait passe les arguments à une autre fonction tout en préservant leur catégorie de valeur (lvalue vs rvalue). T&& dans un contexte déduit est une 'forwarding reference' (pas une référence rvalue). std::forward<T> convertit conditionnellement : lvalue si T est T&, rvalue si T est T&&. Essentiel pour les fonctions factory et les wrappers.

cpp
#include <utility>
template <typename T, typename Arg>
auto make_unique(Arg&& arg) {
    return std::unique_ptr<T>(new T(std::forward<Arg>(arg)));
}

template <typename... Args>
void log(Args&&... args) {
    // std::forward preserves value category
    log_impl(std::forward<Args>(args)...);
}

std::string s = "hello";
log(s);              // Arg = std::string&  (lvalue)
log(std::string());  // Arg = std::string&& (rvalue)

Règle des cinq / Règle de zéro

Règle des cinq : si une classe gère une ressource, vous devez définir destructeur, constructeur de copie, affectation de copie, constructeur de déplacement, affectation de déplacement. Règle de zéro : préférez composer des types RAII (vector, string, pointeurs intelligents) afin que les membres spéciaux générés par le compilateur soient corrects. Cela élimine la gestion manuelle des ressources sujette aux bugs.

cpp
// Rule of Five: if you define any of these, define all 5
class Resource {
    int* data;
public:
    Resource(size_t n) : data(new int[n]) {}
    ~Resource() { delete[] data; }
    Resource(const Resource& o);             // copy ctor
    Resource& operator=(const Resource& o);  // copy assign
    Resource(Resource&& o) noexcept;         // move ctor
    Resource& operator=(Resource&& o) noexcept;  // move assign
};

// Rule of Zero: prefer to use RAII types
class Better {
    std::vector<int> data;  // handles everything
public:
    Better() = default;  // compiler-generated funcs are correct
};

Optimisation de la valeur de retour (RVO/NRVO)

RVO/NRVO permet au compilateur de construire la valeur de retour directement dans le stockage de l'appelant, évitant entièrement les copies/déplacements. C++17 rend RVO obligatoire pour les prvalues. N'écrivez jamais return std::move(local) — cela inhibe NRVO et force un déplacement (plus lent). Renvoyez simplement le local par nom et laissez le compilateur optimiser.

cpp
std::vector<int> makeVec() {
    std::vector<int> v;
    v.push_back(1); v.push_back(2);
    return v;  // NRVO: no copy, no move!
}

std::string makeStr() {
    return std::string("hello");  // RVO
}

// C++17 guarantees RVO (mandatory copy elision) for prvalues
std::vector<int> v = makeVec();  // constructed in place

// Don't std::move return values—it disables RVO!
std::vector<int> bad() {
    std::vector<int> v;
    return std::move(v);  // WORSE: forces move, blocks NRVO
}
15

Concurrence (thread, mutex, async)

Bases de std::thread

std::thread lance un nouveau thread OS. Vous DEVEZ appeler join() (attendre) ou detach() (le laisser tourner indépendamment) avant que l'objet thread ne soit détruit, sinon std::terminate est appelé. Les arguments sont passés par valeur par défaut — utilisez std::ref pour les références, std::move pour les types à déplacement seul. Préférez join sauf si vous avez une raison claire de détacher.

cpp
#include <thread>
#include <iostream>

void worker(int id) {
    std::cout << "Thread " << id << "\n";
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2([]{ std::cout << "Lambda thread\n"; });

    t1.join();   // wait for t1 to finish
    t2.join();   // must join or detach before destruction
    // t1.detach();  // runs independently (risky)

    // Hardware concurrency hint
    unsigned n = std::thread::hardware_concurrency();
}

Mutex et lock guard

Protégez toujours les données partagées avec un mutex. std::lock_guard est le verrou RAII le plus simple — acquiert à la construction, libère à la destruction. std::scoped_lock (C++17) verrouille plusieurs mutex en sécurité avec un algorithme d'évitement d'interblocage. std::unique_lock offre plus de flexibilité (verrouillage/déverrouillage manuel, verrouillage différé) pour une utilisation avec les variables de condition.

cpp
#include <mutex>
#include <vector>

std::mutex mtx;
std::vector<int> shared;

void safePush(int val) {
    // RAII lock: unlocks even if exception thrown
    std::lock_guard<std::mutex> lock(mtx);
    shared.push_back(val);
}  // lock released here

// std::scoped_lock (C++17) locks multiple mutexes deadlock-free
std::mutex m1, m2;
void transfer() {
    std::scoped_lock lock(m1, m2);  // atomic
}

// std::unique_lock: lockable/unlockable, movable
std::unique_lock<std::mutex> ul(mtx);
ul.unlock();
ul.lock();

Variables de condition

condition_variable permet aux threads d'attendre une condition. Utilisez toujours un prédicat avec wait() pour gérer les réveils parasites. Le mutex doit être détenu par un unique_lock lors de l'appel à wait(), qui le libère pendant l'attente et le réacquiert avant de revenir. notify_one réveille un attendant, notify_all réveille tous. Ce pattern implémente des files thread-safe et des pipelines producteur-consommateur.

cpp
#include <condition_variable>
#include <queue>
#include <thread>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> tasks;

void producer() {
    {
        std::lock_guard<std::mutex> lock(mtx);
        tasks.push(42);
    }
    cv.notify_one();  // wake one waiting consumer
}

void consumer() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return !tasks.empty(); });  // predicate prevents spurious wakeup
    int task = tasks.front();
    tasks.pop();
}

std::async et futures

std::async est une façon de haut niveau d'exécuter des tâches asynchrones, renvoyant une future. std::launch::async force un nouveau thread ; std::launch::deferred s'exécute paresseusement sur get(). La politique par défaut peut choisir l'un ou l'autre — soyez explicite pour un comportement prévisible. Pour plus de contrôle, utilisez des paires std::promise/future. Appelez toujours get() sur une future avant destruction, sinon le destructeur peut bloquer.

cpp
#include <future>
#include <iostream>

int slowComputation() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 42;
}

// async policy: may run in new thread or deferred
auto fut = std::async(std::launch::async, slowComputation);
// do other work...
int result = fut.get();  // blocks until ready

// std::launch::async: definitely new thread
// std::launch::deferred: lazy, runs on get()
// std::launch::async | deferred: implementation chooses

// std::promise for manual control
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread([&p]{ p.set_value(10); }).detach();
f.get();  // 10

Opérations atomiques

std::atomic fournit des opérations thread-safe sans verrou pour les types primitifs. Plus lourd qu'un simple int mais beaucoup plus léger qu'un mutex pour des compteurs/drapeaux simples. L'ordre mémoire affecte la visibilité : relaxed (pas d'ordre), acquire/release (paire pour la synchronisation), seq_cst (par défaut, le plus fort). Utilisez les atomiques pour les compteurs/drapeaux ; utilisez mutex pour les sections critiques complexes.

cpp
#include <atomic>
#include <iostream>

std::atomic<int> counter{0};
std::atomic<bool> ready{false};

void worker() {
    while (!ready.load(std::memory_order_acquire));
    counter.fetch_add(1, std::memory_order_relaxed);
}

// Compare-and-swap (CAS) loop
std::atomic<int> val{0};
int expected = 0;
bool success = val.compare_exchange_weak(
    expected, 1,
    std::memory_order_acq_rel);

// Atomic is lock-free for most primitive types
static_assert(std::atomic<int>::is_always_lock_free);

// std::atomic_flag: guaranteed lock-free, for spinlocks
std::atomic_flag spin = ATOMIC_FLAG_INIT;
while (spin.test_and_set(std::memory_order_acquire));  // acquire
spin.clear(std::memory_order_release);  // release
16

Métaprogrammation de templates

Spécialisation de template

La spécialisation de template fournit des implémentations personnalisées pour des types spécifiques. La spécialisation complète fixe tous les paramètres de template. La spécialisation partielle (uniquement pour les templates de classe) spécialise certains paramètres tout en gardant les autres génériques. Largement utilisé dans les type traits, std::vector<bool> et l'optimisation pour des types connus.

cpp
// Primary template
template <typename T>
struct TypeName {
    static const char* get() { return "unknown"; }
};

// Full specialization for int
template <>
struct TypeName<int> {
    static const char* get() { return "int"; }
};

// Full specialization for const char*
template <>
struct TypeName<const char*> {
    static const char* get() { return "string"; }
};

std::cout << TypeName<int>::get();           // "int"
std::cout << TypeName<double>::get();        // "unknown"
std::cout << TypeName<const char*>::get();   // "string"

SFINAE et enable_if

SFINAE (Substitution Failure Is Not An Error) permet d'activer/désactiver des surcharges de template basées sur des propriétés de type. std::enable_if définit conditionnellement un type. Lorsque la substitution échoue, la surcharge est silencieusement supprimée au lieu de causer une erreur. Le if constexpr du C++17 et les concepts du C++20 remplacent souvent SFINAE avec une syntaxe plus propre.

cpp
#include <type_traits>

// SFINAE: Substitution Failure Is Not An Error
// Only enable this overload if T is integral
template <typename T,
          typename = std::enable_if_t<std::is_integral_v<T>>>
void process(T x) { std::cout << "integral\n"; }

// Only enable if T is floating point
template <typename T,
          std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
void process(T x) { std::cout << "floating\n"; }

process(10);    // integral
process(3.14);  // floating

// void_t trick (C++17) for detecting member existence
template <typename T, typename = void>
struct has_size : std::false_type {};
template <typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>>
    : std::true_type {};

if constexpr (C++17)

if constexpr évalue la condition à la compilation et rejette entièrement la branche fausse (pas même vérifiée par le type). Cela remplace de nombreux patterns SFINAE avec un code beaucoup plus propre. Particulièrement utile pour la récursion de template (le cas de base termine la récursion) et pour brancher sur les type traits sans erreurs d'instanciation.

cpp
#include <type_traits>
template <typename T>
auto getValue(T t) {
    if constexpr (std::is_pointer_v<T>) {
        return *t;  // only compiled if T is a pointer
    } else if constexpr (std::is_integral_v<T>) {
        return t * 2;
    } else {
        return t;
    }
}

int x = 5;
int* p = &x;
getValue(x);   // 10
getValue(p);   // 5
getValue(3.14);  // 3.14

// Compile-time recursion with termination
template <int N>
constexpr int factorial() {
    if constexpr (N <= 1) return 1;
    else return N * factorial<N - 1>();
}

Templates variadiques et fold expressions

Les templates variadiques acceptent un nombre quelconque d'arguments via les parameter packs (typename... Args). Les fold expressions du C++17 appliquent un opérateur à tous les éléments du pack : fold unaire (... op pack), fold binaire (init op ... op pack). Avant C++17, vous aviez besoin de récursion avec un cas de base. Les templates variadiques alimentent std::make_unique, std::tuple, les fonctions de type printf.

cpp
#include <iostream>

// Parameter pack
template <typename... Args>
void print(Args... args) {
    // C++17 fold expression
    ((std::cout << args << " "), ...);
    std::cout << "\n";
}

print(1, "hello", 3.14, 'x');  // 1 hello 3.14 x

// Sum with fold
template <typename... T>
auto sum(T... args) {
    return (args + ...);  // binary fold: ((a+b)+c)+d
}

// Sum with initial value
template <typename... T>
auto sumFrom0(T... args) {
    return (0 + ... + args);  // left fold with init
}

// Base case recursion (pre-C++17)
template <typename T>
void printOne(T t) { std::cout << t; }

Concepts (C++20)

Les concepts (C++20) remplacent SFINAE avec des contraintes lisibles et révélatrices d'intention. Ils produisent de bien meilleurs messages d'erreur que SFINAE. Définissez les concepts avec concept Name = constraint;. Utilisez-les dans les paramètres de template, les clauses requires, ou les templates abrégés (auto avec concept). La bibliothèque standard fournit de nombreux concepts utiles dans <concepts>.

cpp
#include <concepts>

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

// Use as constraint
template <Numeric T>
T add(T a, T b) { return a + b; }

// Requires clause
template <typename T>
requires requires(T t) { t.size(); }
auto getSize(const T& t) { return t.size(); }

// Concepts in abbreviated function templates
void process(std::integral auto x) { /* ... */ }
void process(std::ranges::range auto& r) { /* ... */ }

// Standard concepts: integral, floating_point, equality_comparable,
// totally_ordered, copyable, movable, default_initializable, etc.
17

Expressions lambda approfondies

Modes de capture

Les lambdas capturent les variables du périmètre englobant. [=] capture tout par valeur, [&] par référence — pratique mais sujet aux erreurs (références pendantes, captures non intentionnelles). Préférez les captures explicites [x, &y] pour la clarté. Les captures d'initialisation [name = expr] (C++14) permettent de renommer, déplacer et calculer les valeurs capturées. Capturez [&] avec précaution — les lambdas survivant au périmètre causent des références pendantes.

cpp
int x = 10, y = 20;
std::vector<int> v;

auto a = [] { /* no capture */ };
auto b = [x] { return x; };           // capture x by value
auto c = [&x] { x = 100; };           // capture x by reference
auto d = [=] { return x + y; };       // capture all by value
auto e = [&] { x = 1; y = 2; };       // capture all by reference
auto f = [x, &y] { return x + y; };   // mixed
auto g = [=, &x] { x = 1; return y; }; // default value, x by ref
auto h = [this] { return member; };   // capture this (C++17: *this)
auto i = [x = x + 5] { return x; };   // init capture (C++14)

// Best practice: capture only what you need, explicitly

Lambdas génériques (C++14)

Les lambdas génériques utilisent des paramètres auto (C++14) ou des paramètres de template explicites (C++20). Ce sont essentiellement des surcharges de operator() template générées par le compilateur. Les lambdas template du C++20 vous permettent d'accéder directement au paramètre de type T. Les lambdas récursifs nécessitent std::function (ou le deducing this du C++23) car un lambda auto simple ne peut pas se référer à lui-même par nom avant que son type soit connu.

cpp
// auto parameters (C++14)
auto add = [](auto a, auto b) { return a + b; };
add(1, 2);        // int
add(1.5, 2.5);    // double
add(std::string("a"), std::string("b"));

// Template lambdas (C++20)
auto getSize = []<typename T>(const std::vector<T>& v) {
    return v.size();
};

// Perfect forwarding in lambdas (C++20)
auto wrapper = []<typename T>(T&& arg) {
    return process(std::forward<T>(arg));
};

// Recursive lambda with std::function
#include <functional>
std::function<int(int)> fib = [&](int n) {
    return n < 2 ? n : fib(n - 1) + fib(n - 2);
};

Lambdas mutable et à état

Par défaut, les variables capturées par valeur dans un lambda sont const — operator() est const. Le mot-clé mutable supprime cette constance, permettant la modification des variables capturées par valeur (modification de la copie, pas de l'original). Cela permet des lambdas à état comme les compteurs. Chaque copie d'un lambda a son propre état capturé. Utilisez std::function pour stocker des lambdas de différents types dans des conteneurs.

cpp
#include <functional>
int counter = 0;

// Without mutable, value-captured vars are const
auto inc = [counter]() mutable {
    return ++counter;  // modifies the captured copy
};
inc(); inc();
std::cout << counter;  // still 0 (original unchanged)

// Stateful counter with std::function
auto makeCounter(int start) {
    return [count = start]() mutable { return ++count; };
}
auto c = makeCounter(10);
c(); c();  // 11, 12

// Lambda as callback storage
std::vector<std::function<void()>> callbacks;
callbacks.push_back([&]{ std::cout << "click"; });

Lambdas avec les algorithmes STL

Les lambdas brillent comme prédicats/comparateurs pour les algorithmes STL. Ils remplacent l'ancienne approche par functor (objet fonction) par du code en ligne et lisible. Usages courants : comparateurs pour sort, prédicats pour find_if/remove_if/copy_if, transformations pour transform. Les ranges du C++20 rendent cela encore plus propre avec l'opérateur pipe pour des transformations composables.

cpp
#include <algorithm>
#include <vector>

std::vector<int> v = {5, 2, 8, 1, 9, 3};

// Sort descending
std::sort(v.begin(), v.end(), [](int a, int b) {
    return a > b;
});

// Find first even
auto it = std::find_if(v.begin(), v.end(),
    [](int x) { return x % 2 == 0; });

// Remove odds (erase-remove idiom)
v.erase(std::remove_if(v.begin(), v.end(),
    [](int x) { return x % 2 != 0; }), v.end());

// Transform
std::transform(v.begin(), v.end(), v.begin(),
    [](int x) { return x * x; });

// C++20 ranges: cleaner
auto evens = v | std::views::filter([](int x){ return x%2==0; })
               | std::views::transform([](int x){ return x*2; });

Fonctions d'ordre supérieur

Les lambdas permettent les patterns de programmation fonctionnelle en C++. Les fonctions peuvent renvoyer des lambdas (fermetures) ou les accepter comme paramètres. std::function enveloppe tout appelable pour un stockage à type effacé. La composition de fonctions construit des pipelines en enchaînant des lambdas. Ce style est puissant pour les rappels, les gestionnaires d'événements et les pipelines de transformation de données, bien que les approches basées sur les templates évitent le surcoût de std::function.

cpp
#include <functional>
// Function returning a function
auto makeMultiplier(int factor) {
    return [factor](int x) { return x * factor; };
}
auto doubleIt = makeMultiplier(2);
auto tripleIt = makeMultiplier(3);
doubleIt(5);  // 10
tripleIt(5);  // 15

// Function taking a function
template <typename F>
void applyTwice(int x, F f) {
    std::cout << f(f(x));
}
applyTwice(5, [](int x){ return x + 3; });  // 11

// Composition
auto compose = [](auto f, auto g) {
    return [f, g](auto x) { return f(g(x)); };
};
auto addOne = [](int x){ return x + 1; };
auto square = [](int x){ return x * x; };
auto sqThenAdd = compose(addOne, square);
sqThenAdd(3);  // 10
18

Namespaces & modules

Bases des namespaces

Les namespaces préviennent les collisions de noms dans les grands projets. Ils peuvent être imbriqués et aliasés pour la commodité. Les namespaces anonymes donnent aux symboles une liaison interne (visibles uniquement dans l'unité de traduction courante), remplaçant le mot-clé static style C à cette fin. Évitez using namespace dans les en-têtes — il pollue chaque fichier qui l'inclut.

cpp
namespace math {
    double pi = 3.14159;
    double square(double x) { return x * x; }

    namespace geometry {  // nested
        double circleArea(double r) { return pi * square(r); }
    }
}

// Usage
double a = math::square(5);
double area = math::geometry::circleArea(2.0);

// Namespace alias
namespace geo = math::geometry;
geo::circleArea(1.0);

// Anonymous namespace: internal linkage (like 'static')
namespace {
    int internalVar = 42;  // only visible in this file
}

Déclarations et directives using

La déclaration using (using std::cout) importe un seul nom — sûr et recommandé. La directive using (using namespace std) importe tout — pratique mais risqué, surtout dans les en-têtes (cause des conflits de noms et des ambiguïtés). Limitez les directives using à un petit périmètre (fichiers de fonction/implémentation) et jamais dans les en-têtes. using enum du C++20 importe tous les énumérateurs d'un enum à portée.

cpp
#include <iostream>
#include <vector>

// using declaration: brings one name
using std::cout;
using std::vector;
cout << "hello";
vector<int> v;

// using directive: brings entire namespace
using namespace std;  // brings ALL of std
// AVOID in headers! Pollutes global namespace.

// Namespace-scoped using (safe)
namespace mylib {
    using std::string;  // only affects mylib
    using std::vector;
    string s;
}

// C++20: using enum
enum class Color { Red, Green, Blue };
void print() {
    using enum Color;
    auto c = Red;  // no Color:: needed here
}

Recherche dépendante des arguments (ADL)

ADL (recherche de Koenig) trouve les fonctions libres basées sur les namespaces de leurs arguments. C'est pourquoi std::cout << x fonctionne sans std::operator<< — le compilateur cherche dans le namespace de x. ADL est essentiel pour les surcharges d'opérateurs et les points de personnalisation comme swap. Le pattern 'using std::swap; swap(a,b);' permet aux types utilisateur de fournir des swaps optimisés tout en retombant sur std::swap.

cpp
namespace mylib {
    struct Widget {
        int value;
    };
    // Operator overload found via ADL
    std::ostream& operator<<(std::ostream& os, const Widget& w) {
        return os << "Widget(" << w.value << ")";
    }

    void helper(const Widget&) {}
}

int main() {
    mylib::Widget w{42};
    // ADL: finds operator<< in mylib without qualification
    std::cout << w;  // works! no mylib:: needed

    // ADL applies to free functions too
    helper(w);  // wait—needs mylib::helper unless ADL applies
    mylib::helper(w);  // explicit
}

// swap is the classic ADL use case
namespace ns { struct X {};
    void swap(X&, X&) {}  // customized swap
}
void f(ns::X& a, ns::X& b) {
    using std::swap;
    swap(a, b);  // ADL picks ns::swap if available
}

Namespaces inline (versionnage)

Les namespaces inline exposent leurs membres comme s'ils étaient dans le namespace englobant. Cela permet le versionnage de bibliothèque : rendez la version la plus récente inline afin que les utilisateurs l'obtiennent par défaut, tandis que les anciennes versions restent accessibles via qualification explicite. C'est aussi utilisé pour la compatibilité ABI et le bascule de fonctionnalités. Changer quel namespace est inline déplace la version par défaut sans modifier le code utilisateur.

cpp
// Inline namespace members are part of the enclosing namespace
namespace mylib {
    inline namespace v2 {
        struct Widget { int x, y, z; };  // newer version
        void process(Widget) {}
    }

    namespace v1 {
        struct Widget { int x, y; };  // older version
    }
}

// v2::Widget is accessible as mylib::Widget (inline)
mylib::Widget w;  // actually mylib::v2::Widget
mylib::process(w);

// Explicitly use v1 if needed
mylib::v1::Widget oldW;

// ABI versioning: change which namespace is inline
// to change default version without breaking old code

Modules C++20

Les modules C++20 remplacent #include par un système plus rapide et plus robuste. export module déclare un module ; export marque les déclarations visibles. Les modules sont compilés une fois (pas réanalysés par unité de traduction), améliorant considérablement les temps de build. Ils évitent la pollution des macros et les problèmes d'ordre d'en-têtes. L'adoption est progressive — le support des toolchains (CMake, systèmes de build) est encore en maturation en 2024.

cpp
// math.cppm (module interface unit)
export module math;

export double pi = 3.14159;

export double square(double x) {
    return x * x;
}

// Internal (not exported)
double internalHelper(double x) {
    return x * 2;
}

// main.cpp
import math;
import std;  // standard library module (C++23)

int main() {
    return square(pi);  // 9.87
    // internalHelper(3);  // ERROR: not exported
}

// Module partitions (sub-modules)
export module math:geometry;
export double circleArea(double r);
19

Préprocesseur & macros

Include guards et #pragma once

Les include guards empêchent un en-tête d'être traité plusieurs fois dans une unité de traduction, évitant les erreurs de redéfinition. #ifndef/#define/#endif est standard et portable. #pragma once est plus simple et évite les collisions de noms de macros mais est techniquement non standard (supporté par tous les compilateurs majeurs). Le code moderne utilise souvent #pragma once pour la simplicité.

cpp
// Traditional include guard (header.h)
#ifndef MY_HEADER_H
#define MY_HEADER_H

// declarations here
class Widget { /* ... */ };

#endif  // MY_HEADER_H

// Alternative: #pragma once (non-standard but widely supported)
#pragma once

class Widget { /* ... */ };

// #pragma once pros: simpler, no risk of macro name collision
// Include guards pros: standard, works everywhere
// Both prevent multiple inclusion in a single TU

Macros de type fonction

Les macros de type fonction sont des substitutions de texte. PARENTHÉSEZ TOUJOURS chaque argument et toute l'expression pour éviter les bugs de précédence. L'idiome do { ... } while (0) fait se comporter une macro comme une seule instruction. Les macros n'ont pas de vérification de type, pas de périmètre, et peuvent avoir des effets de bord (MAX(i++, j++) incrémente deux fois). Préférez constexpr/inline/templates en C++ moderne.

cpp
// Basic macro
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))

int m = MAX(3, 5);      // ((3) > (5) ? (3) : (5))
int s = SQUARE(4);      // ((4) * (4))

// ALWAYS parenthesize arguments!
#define BAD(x) x * x
BAD(1 + 2);  // 1 + 2 * 1 + 2 = 5, not 9!

// Multi-line macro with do-while
#define LOG(msg) do { \
    std::cerr << __FILE__ << ":" << __LINE__ \
              << " " << msg << "\n"; \
} while (0)

// Variadic macro (C++11)
#define PRINT(...) printf(__VA_ARGS__)
PRINT("x=%d, y=%d\n", x, y);

Stringification et concaténation

# (stringification) convertit un argument de macro en littéral de chaîne. ## (token pasting) concatène deux jetons en un. L'astuce à deux niveaux STR/XSTR développe d'abord les macros puis les stringifie. Ces opérateurs sont essentiels pour la génération de code, les macros de journalisation et les X-macros. Le C++ moderne remplace souvent cela par des fonctions constexpr, mais ils restent utiles pour la génération de chaînes/jetons à la compilation.

cpp
// # (stringification) turns a macro argument into a string literal
#define STR(x) #x
#define XSTR(x) STR(x)
const char* s1 = STR(hello world);  // "hello world"
const char* s2 = STR(42);           // "42"

// Two-level macro to expand macros before stringifying
#define VERSION 100
const char* v1 = STR(VERSION);   // "VERSION"
const char* v2 = XSTR(VERSION);  // "100"

// ## (token pasting) concatenates tokens
#define CONCAT(a, b) a ## b
#define MAKE_VAR(n) var_ ## n
int MAKE_VAR(1) = 10;  // int var_1 = 10;
int CONCAT(foo, bar) = 5;  // int foobar = 5;

// Useful for generating unique names
#define UNIQUE(prefix) prefix ## __LINE__
int UNIQUE(tmp_) = 0;  // int tmp_42 = 0; (if on line 42)

Compilation conditionnelle

La compilation conditionnelle (#if, #ifdef, #ifndef, #elif, #else, #endif) inclut/exclut du code à la compilation. Utilisée pour le code spécifique à une plateforme, les builds de débogage et les drapeaux de fonctionnalité. defined(X) vérifie si X est défini (valeur sans importance). #error abandonne la compilation avec un message. Préférez constexpr if lorsque possible — c'est de type sûr et le code est toujours compilé (détectant les erreurs dans toutes les configurations).

cpp
#define DEBUG 1
#define PLATFORM "windows"

#if DEBUG
    std::cout << "Debug mode\n";
#endif

#ifdef DEBUG
    logDebug("entered function");
#endif

#ifndef NDEBUG
    assert(ptr != nullptr);
#endif

#if defined(_WIN32) || defined(_WIN64)
    #include <windows.h>
#elif defined(__linux__)
    #include <unistd.h>
#elif defined(__APPLE__)
    #include <TargetConditionals.h>
#endif

// Check C++ standard
#if __cplusplus >= 202002L
    #define CPP20_OR_LATER
#elif __cplusplus >= 201703L
    #define CPP17_OR_LATER
#endif

// #error and #warning
#if !defined(VERSION)
    #error "VERSION must be defined"
#endif

Macros prédéfinies et __VA_OPT__

Les macros prédéfinies fournissent des informations à la compilation : __FILE__/__LINE__ pour la journalisation, __cplusplus pour la version du standard, __func__ pour les noms de fonction (c'est un identifiant, pas une macro). __VA_OPT__ (C++20) insère conditionnellement une virgule uniquement lorsque __VA_ARGS__ est non vide, corrigeant le problème de la virgule finale dans les macros variadiques. __has_include (C++17) permet l'inclusion optionnelle d'en-têtes pour la portabilité.

cpp
// Standard predefined macros
std::cout << __FILE__;       // source file path
std::cout << __LINE__;       // current line number
std::cout << __DATE__;       // compilation date "Mmm dd yyyy"
std::cout << __TIME__;       // compilation time "hh:mm:ss"
std::cout << __cplusplus;    // C++ standard version
std::cout << __func__;       // current function name (not macro)

// Compiler-specific
#ifdef __GNUC__
    std::cout << __GNUC__ << "." << __GNUC_MINOR__;
#endif
#ifdef _MSC_VER
    std::cout << "MSVC " << _MSC_VER;
#endif

// __VA_OPT__ (C++20): expands to its arg if variadic has args
#define LOG(fmt, ...) \
    printf(fmt __VA_OPT__(,) __VA_ARGS__)
LOG("plain");              // printf("plain")
LOG("x=%d", x);            // printf("x=%d", x)

// __has_include (C++17): check if header exists
#if __has_include(<optional>)
    #include <optional>
#endif
20

Design patterns en C++

Singleton (Singleton de Meyers)

Singleton garantit qu'une classe a une instance avec un accès global. Le Singleton de Meyers (variable locale statique) est l'implémentation C++ la plus propre — l'initialisation thread-safe est garantie depuis C++11. Supprimez les opérations de copie pour prévenir la duplication. Les singletons sont controversés (état global, difficiles à tester) ; préférez l'injection de dépendances lorsque c'est possible. Utilisez-le lorsqu'une seule instance est réellement nécessaire (logger, config, interface matérielle).

cpp
class Logger {
public:
    static Logger& instance() {
        // Meyers' singleton: thread-safe in C++11+
        static Logger inst;
        return inst;
    }
    void log(const std::string& msg) {
        std::cout << "[LOG] " << msg << "\n";
    }
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
private:
    Logger() = default;
};

// Usage
Logger::instance().log("started");

// Avoid: lazy init with new + manual sync (error-prone)
// The static local is initialized once, thread-safely

RAII (Resource Acquisition Is Initialization)

RAII est l'idiome le plus important du C++ : acquérir les ressources dans les constructeurs, libérer dans les destructeurs. Cela garantit le nettoyage même lorsque des exceptions sont lancées. Les pointeurs intelligents, std::lock_guard, std::fstream et std::vector utilisent tous RAII. Le destructeur s'exécute pendant le déroulement de la pile, rendant le code sûr vis-à-vis des exceptions naturel. RAII élimine le new/delete, lock/unlock, open/close manuels — adoptez-le partout.

cpp
// RAII: resource tied to object lifetime
class FileHandle {
    FILE* fp;
public:
    explicit FileHandle(const char* path) : fp(fopen(path, "r")) {
        if (!fp) throw std::runtime_error("open failed");
    }
    ~FileHandle() { if (fp) fclose(fp); }
    FILE* get() { return fp; }
    // disable copy to prevent double-close
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    // allow move
    FileHandle(FileHandle&& o) noexcept : fp(o.fp) { o.fp = nullptr; }
};

{
    FileHandle f("data.txt");
    // use f.get()
}  // fclose called automatically, even on exception

Idiome Pimpl (Pointer to Implementation)

Pimpl (Pointer to Implementation) cache les détails d'implémentation derrière un pointeur. Avantages : compilation plus rapide (les membres privés n'apparaissent pas dans l'en-tête), véritable stabilité ABI (modifier Impl ne casse pas l'ABI), dépendances d'inclusion réduites. Le destructeur doit être défini dans le fichier .cpp car unique_ptr<Impl> nécessite que Impl soit complet. Courant dans la conception de bibliothèques pour des APIs stables.

cpp
// widget.h - public interface
class Widget {
public:
    Widget();
    ~Widget();  // must define in .cpp (unique_ptr needs complete type)
    Widget(Widget&&) noexcept;
    Widget& operator=(Widget&&) noexcept;
    void doSomething();
private:
    class Impl;  // forward declaration
    std::unique_ptr<Impl> pimpl;
};

// widget.cpp - implementation
#include "widget.h"
#include <vector>
class Widget::Impl {
public:
    std::vector<int> data;
    void doSomething() { /* ... */ }
};
Widget::Widget() : pimpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
Widget::Widget(Widget&&) noexcept = default;
void Widget::doSomething() { pimpl->doSomething(); }

Pattern Observer

Observer permet aux sujets de notifier les abonnés des changements sans couplage. std::function facilite l'acceptation de lambdas, de fonctions membres et de functors. Pour le code de production, envisagez un mécanisme de désabonnement basé sur des jetons (renvoyez un ID à l'abonnement, utilisez-le pour supprimer). Attention aux références pendantes si les observateurs capturent [&] et survivent au sujet. Les signals/slots de Qt et les events de C# sont des implémentations matures.

cpp
#include <functional>
#include <vector>
#include <string>

class Subject {
    std::vector<std::function<void(const std::string&)>> observers;
public:
    void subscribe(std::function<void(const std::string&)> cb) {
        observers.push_back(cb);
    }
    void notify(const std::string& event) {
        for (auto& cb : observers) cb(event);
    }
};

// Usage
Subject s;
s.subscribe([](const std::string& e){ std::cout << "A: " << e; });
s.subscribe([](const std::string& e){ std::cout << "B: " << e; });
s.notify("hello");  // both callbacks invoked

// For removal, use a token/ID system or observer objects
// instead of std::function

CRTP (Curiously Recurring Template Pattern)

CRTP (Derived : Base<Derived>) réalise le polymorphisme statique — la classe de base personnalise le comportement via le type dérivé. Pas de surcoût de fonction virtuelle. Utilisé pour ajouter des fonctionnalités (Comparable, Iterable) aux classes dérivées via mixins. Le downcast via static_cast est sûr car le template garantit le type dérivé. CRTP alimente std::enable_shared_from_this, std::iterator et de nombreux designs basés sur des politiques.

cpp
// CRTP: class Derived : public Base<Derived>
template <typename Derived>
struct Comparable {
    bool operator==(const Derived& other) const {
        return static_cast<const Derived*>(this)->equalTo(other);
    }
    bool operator!=(const Derived& other) const {
        return !(*this == other);
    }
};

struct Point : Comparable<Point> {
    int x, y;
    bool equalTo(const Point& o) const { return x == o.x && y == o.y; }
};

Point a{1, 2}, b{1, 2};
a == b;  // true (uses Comparable::operator==)
a != b;  // false

// Static polymorphism (no virtual overhead)
template <typename T>
void draw(const T& shape) {
    static_cast<const T&>(shape).drawImpl();
}
21

Pointeurs intelligents approfondis

unique_ptr

unique_ptr est le propriétaire exclusif de son objet. Ne peut pas être copié, seulement déplacé. Se supprime automatiquement hors périmètre. make_unique est la façon préférée de créer. Surcoût nul par rapport aux pointeurs bruts.

cpp
#include <memory>
std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1;  // Error: cannot copy
std::unique_ptr<int> p3 = std::move(p1);  // OK: transfer ownership
// p1 is now nullptr

shared_ptr

shared_ptr permet plusieurs propriétaires via le comptage de références. use_count() montre le nombre de propriétaires. Thread-safe pour le compteur mais pas pour l'objet. Utilisez make_shared pour l'efficacité (allocation unique).

cpp
auto p1 = std::make_shared<int>(42);
auto p2 = p1;  // OK: shared ownership
std::cout << p1.use_count();  // 2
// Reference counting: deleted when count reaches 0
// Thread-safe for reference count, not for the object

weak_ptr

weak_ptr est une référence non propriétaire à shared_ptr. Prévient les références circulaires (fuites de mémoire). lock() essaie de convertir en shared_ptr. Utilisez expired() pour vérifier si l'objet existe encore. N'affecte pas le compteur de références.

cpp
auto shared = std::make_shared<int>(42);
std::weak_ptr<int> weak = shared;  // Does not increase count
if (auto locked = weak.lock()) {  // Try to get shared_ptr
    std::cout << *locked;  // Use the object
} else {
    std::cout << "Object deleted";
}

Deleter personnalisé

Les deleteurs personnalisés permettent à unique_ptr de gérer des ressources non mémoire comme les handles de fichiers, les sockets et les APIs C. Le type du deleter fait partie du type unique_ptr. Utile pour RAII avec des bibliothèques C.

cpp
std::unique_ptr<FILE, decltype(&fclose)> file(fopen("test.txt", "r"), fclose);
// Or with lambda
auto deleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(deleter)> file2(fopen("test.txt", "r"), deleter);

enable_shared_from_this

enable_shared_from_this permet à un objet d'obtenir en sécurité un shared_ptr vers lui-même. Appeler shared_ptr(this) directement créerait un second compteur de références, causant une double suppression. Héritez de enable_shared_from_this et utilisez shared_from_this().

cpp
class Node : public std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> getPtr() {
        return shared_from_this();  // Safe
        // return std::shared_ptr<Node>(this);  // BUG: double delete
    }
};
22

Sémantique de déplacement approfondie

Constructeur de déplacement

Les constructeurs de déplacement volent les ressources au lieu de copier. noexcept est important : les conteneurs STL ne déplacent (pas copient) que si le déplacement est noexcept. Laissez toujours l'objet déplacé dans un état valide.

cpp
class Buffer {
    int* data; size_t size;
public:
    Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
        other.data = nullptr; other.size = 0;  // Leave in valid state
    }
    Buffer& operator=(Buffer&& other) noexcept {
        delete[] data;
        data = other.data; size = other.size;
        other.data = nullptr; other.size = 0;
        return *this;
    }
};

std::move

std::move est une conversion vers une référence rvalue. Il n'effectue pas le déplacement lui-même. Le constructeur de déplacement ou l'opérateur d'affectation fait le travail réel. Après std::move, l'objet source est dans un état valide mais non spécifié.

cpp
std::string s1 = "Hello";
std::string s2 = std::move(s1);  // s1 is now empty
// std::move does not move anything!
// It casts to an rvalue reference, enabling move
// The actual move happens in the constructor/assignment

Transfert parfait

Le transfert parfait préserve la catégorie de valeur des arguments. T&& dans un template est une forwarding reference, se liant aux lvalues et rvalues. std::forward<T> reconvertit vers la catégorie d'origine. Essentiel pour les fonctions factory et les wrappers.

cpp
template<typename T, typename Arg>
auto make(Arg&& arg) {
    return std::make_shared<T>(std::forward<Arg>(arg));
}
// std::forward preserves value category:
// lvalue stays lvalue, rvalue stays rvalue
// Arg&& is a forwarding reference (not rvalue reference)

RVO & NRVO

RVO (Return Value Optimization) et NRVO (Named RVO) éliminent les copies en construisant l'objet en place. Les compilateurs effectuent cela automatiquement. Utiliser std::move sur une valeur de retour locale empêche NRVO et peut pessimeriser. Faites confiance au compilateur.

cpp
std::string create() {
    std::string s = "Hello";
    return s;  // NRVO: no copy, no move
}
std::string s = create();  // RVO: no copy
// With -O2, compilers eliminate the copy/move entirely
// Do NOT use std::move on return of local variable!

Règle des cinq

Règle des cinq : si vous définissez un destructeur, un constructeur/affectation de copie, ou un constructeur/affectation de déplacement, définissez les cinq. Cela assure une gestion correcte des ressources. La Règle de zéro est préférée : utilisez des types RAII (pointeurs intelligents, vectors) pour éviter la gestion manuelle.

cpp
class Resource {
public:
    Resource();  // Constructor
    ~Resource();  // Destructor
    Resource(const Resource&);  // Copy constructor
    Resource& operator=(const Resource&);  // Copy assignment
    Resource(Resource&&) noexcept;  // Move constructor
    Resource& operator=(Resource&&) noexcept;  // Move assignment
};
23

Templates approfondis

Templates variadiques

Les templates variadiques acceptent un nombre quelconque d'arguments. Les fold expressions (C++17) simplifient le dépaquetage. L'approche récursive fonctionne en C++11. sizeof...(args) donne le comptage. Utilisés dans tuple, make_shared et les remplacements de printf.

cpp
template<typename... Args>
void print(Args... args) {
    (std::cout << ... << args) << '\n';  // C++17 fold expression
}
print(1, "hello", 3.14, 'a');  // 1hello3.14a
// Recursive unpacking (pre-C++17)
template<typename T, typename... Rest>
void print(T first, Rest... rest) {
    std::cout << first;
    print(rest...);
}

SFINAE

SFINAE (Substitution Failure Is Not An Error) retire les surcharges invalides de la considération. enable_if ajoute une condition au template. Si la condition est fausse, la spécialisation est ignorée. if constexpr du C++17 est souvent plus propre.

cpp
template<typename T,
         typename = std::enable_if_t<std::is_integral_v<T>>>
void process(T value) { /* integer version */ }
template<typename T,
         typename = std::enable_if_t<std::is_floating_point_v<T>>,
         typename = void>  // Different signature
void process(T value) { /* float version */ }

if constexpr

if constexpr (C++17) évalue les conditions à la compilation. Seule la branche vraie est compilée, l'autre est rejetée. Beaucoup plus propre que SFINAE pour la compilation conditionnelle. Fonctionne avec la déduction du type de retour auto.

cpp
template<typename T>
auto get_value(T t) {
    if constexpr (std::is_pointer_v<T>)
        return *t;
    else
        return t;
}
// Only the matching branch is compiled
// No SFINAE needed

Concepts (C++20)

Les concepts (C++20) contraignent les paramètres de template avec une syntaxe lisible. Ils fournissent de meilleurs messages d'erreur que SFINAE. Utilisez les concepts existants (integral, floating_point) ou définissez-en des personnalisés. Les concepts peuvent être combinés avec && et ||.

cpp
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template<Number T>
T add(T a, T b) { return a + b; }
// Or: requires clause
template<typename T> requires Number<T>
T multiply(T a, T b) { return a * b; }

Spécialisation de template

La spécialisation complète fournit une implémentation complète pour un type spécifique. La spécialisation partielle personnalise pour une catégorie (par ex., tous les pointeurs). Le template primaire doit être déclaré d'abord. Les spécialisations doivent correspondre à l'interface.

cpp
template<typename T>
class Vector { /* general implementation */ };
template<>
class Vector<bool> {  // Full specialization
    // Bit-packed implementation
};
template<typename T>
class Vector<T*> {  // Partial specialization for pointers
    // Pointer-specific implementation
};
24

Algorithmes STL approfondis

sort & stable_sort

sort est O(n log n), non stable. stable_sort préserve l'ordre relatif des éléments égaux. Utilisez un comparateur pour un ordre personnalisé. Les ranges du C++20 permettent : std::ranges::sort(v, {}, &last_digit).

cpp
std::vector<int> v = {3, 1, 4, 1, 5, 9};
std::sort(v.begin(), v.end());  // 1,1,3,4,5,9
std::sort(v.begin(), v.end(), std::greater<>());  // Descending
// Custom comparator
std::sort(v.begin(), v.end(), [](int a, int b) {
    return a % 10 < b % 10;  // Sort by last digit
});

transform & accumulate

transform applique une fonction à chaque élément. accumulate replie les éléments avec une opération (+ par défaut). La valeur initiale détermine le type. Utilisez std::reduce (C++17) pour une réduction parallèle.

cpp
std::vector<int> v = {1, 2, 3, 4};
std::vector<int> squared(v.size());
std::transform(v.begin(), v.end(), squared.begin(),
    [](int x) { return x * x; });  // 1,4,9,16
int sum = std::accumulate(v.begin(), v.end(), 0);  // 10
int product = std::accumulate(v.begin(), v.end(), 1, std::multiplies<>());

find & count

find renvoie un itérateur vers la première correspondance ou end(). count renvoie le nombre de correspondances. find_if utilise un prédicat. Tous sont O(n). Pour les plages triées, utilisez binary_search, lower_bound, upper_bound (O(log n)).

cpp
std::vector<int> v = {1, 2, 3, 2, 1};
auto it = std::find(v.begin(), v.end(), 2);  // First 2
size_t cnt = std::count(v.begin(), v.end(), 2);  // 2
auto it2 = std::find_if(v.begin(), v.end(),
    [](int x) { return x > 2; });  // First > 2

copy & remove

copy_if copie les éléments correspondants. back_inserter ajoute à la destination. remove ne supprime pas réellement ; il déplace les éléments non correspondants vers l'avant et renvoie une nouvelle fin. erase supprime le reste. C++20 ajoute std::erase pour les conteneurs.

cpp
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> evens;
std::copy_if(v.begin(), v.end(), std::back_inserter(evens),
    [](int x) { return x % 2 == 0; });  // 2,4
// Remove-erase idiom
v.erase(std::remove(v.begin(), v.end(), 3), v.end());
// C++20: std::erase(v, 3);

minmax & clamp

minmax renvoie une paire de (min, max) en un seul passage. clamp restreint une valeur à une plage. min/max acceptent des initializer lists ou deux valeurs. Tous acceptent des comparateurs personnalisés. Les liaisons structurées (C++17) simplifient le résultat.

cpp
auto [min, max] = std::minmax({3, 1, 4, 1, 5});  // C++17
int value = std::clamp(15, 0, 10);  // 10 (clamped to max)
int m = std::min({1, 2, 3});
int m2 = std::max({1, 2, 3}, [](int a, int b) { return a < b; });
25

Concurrence

std::thread

std::thread crée des threads OS. join() attend la fin (blocant). detach() tourne indépendamment (peut survivre au créateur). Un thread sans les deux cause std::terminate. Passez les arguments par valeur ou utilisez std::ref pour les références.

cpp
#include <thread>
void task(int n) { /* ... */ }
std::thread t1(task, 42);
std::thread t2([]() { /* lambda */ });
t1.join();  // Wait for completion
t2.detach();  // Run independently
// Always join or detach before destruction

std::mutex

lock_guard est un RAII simple : verrouille à la construction, déverrouille à la destruction. unique_lock est plus flexible : peut déverrouiller/ reverrouiller, utilisé avec les variables de condition. Ne déverrouillez jamais manuellement avec lock_guard. Utilisez std::scoped_lock pour plusieurs mutex.

cpp
std::mutex mtx;
int shared = 0;
void increment() {
    std::lock_guard<std::mutex> lock(mtx);  // RAII
    ++shared;
}  // Auto-unlock
// std::unique_lock for conditional locking
std::unique_lock<std::mutex> ulock(mtx);
ulock.unlock();  // Manual unlock
ulock.lock();    // Re-lock

std::async & futures

std::async exécute une fonction asynchronement, renvoyant une future. get() bloque et récupère le résultat. launch::async force un nouveau thread. launch::deferred s'exécute synchronement sur get(). Le défaut peut être l'un ou l'autre. Les exceptions sont propagées via get().

cpp
#include <future>
std::future<int> f = std::async(std::launch::async, []() {
    return 42;
});
int result = f.get();  // Blocks until ready
// std::launch::async: always new thread
// std::launch::deferred: lazy, runs on get()

Variable de condition

condition_variable synchronise les threads. wait libère le verrou et bloque jusqu'à notification. Le prédicat gère les réveils parasites. notify_one réveille un attendant, notify_all réveille tous. Modifiez toujours l'état partagé sous le mutex avant de notifier.

cpp
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
// Waiter
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []() { return ready; });  // Spurious wakeups handled
// Notifier
{
    std::lock_guard<std::mutex> lk(mtx);
    ready = true;
}
cv.notify_one();  // Or notify_all()

Opérations atomiques

atomic fournit des opérations thread-safe sans verrou. fetch_add/sub sont des lecture-modification-écriture. compare_exchange implémente CAS (compare-and-swap). memory_order_relaxed est le plus rapide mais le plus faible. seq_cst (par défaut) est le plus fort. Utilisez les atomiques pour les compteurs et drapeaux simples.

cpp
#include <atomic>
std::atomic<int> counter{0};
counter++;  // Atomic increment
counter.fetch_add(1, std::memory_order_relaxed);
bool expected = false;
counter.compare_exchange_strong(expected, true);
// Memory orders: relaxed, acquire, release, seq_cst
26

Fonctionnalités du C++ moderne

Liaisons structurées

Les liaisons structurées (C++17) décomposent les paires, tuples et structs. auto& pour les références, auto pour les copies. Simplifie l'itération sur les maps. Fonctionne avec tout type agrégat. Beaucoup plus propre que .first/.second.

cpp
std::pair p = {1, "hello"};
auto [num, str] = p;  // C++17
std::map<std::string, int> m = {{"a", 1}, {"b", 2}};
for (const auto& [key, value] : m) {
    std::cout << key << ": " << value;
}

std::optional

optional représente une valeur qui peut ou non exister. Mieux que les pointeurs ou les valeurs sentinelles. has_value() ou operator bool vérifie. value() lance si vide, value_or() fournit une valeur par défaut. Utilisez-le pour les fonctions qui peuvent échouer à produire un résultat.

cpp
#include <optional>
std::optional<int> find(bool found) {
    if (found) return 42;
    return std::nullopt;
}
auto result = find(true);
if (result) std::cout << *result;
// Or: result.value_or(0)

std::variant

variant est une union de type sûr. Contient l'un de plusieurs types. visit applique un visiteur (lambda surchargé). index() renvoie l'index du type courant. get_if<T> récupère en sécurité. Remplace les unions et l'héritage pour les hiérarchies de types fermées.

cpp
#include <variant>
std::variant<int, std::string> v;
v = 42;
v = "hello";
std::visit([](auto&& arg) {
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>)
        std::cout << "int: " << arg;
    else if constexpr (std::is_same_v<T, std::string>)
        std::cout << "string: " << arg;
}, v);

Ranges (C++20)

Les ranges (C++20) fournissent des vues composables et paresseuses. L'opérateur | enchaîne les opérations. Pas de conteneurs intermédiaires. Les vues sont paresseuses : le calcul se fait à l'itération. Beaucoup plus lisible que les appels d'algorithme imbriqués.

cpp
#include <ranges>
namespace rv = std::ranges::views;
auto result = std::vector{1, 2, 3, 4, 5}
    | rv::filter([](int x) { return x % 2 == 0; })
    | rv::transform([](int x) { return x * x; });
// Lazy evaluation: no computation until iterated
for (int x : result) std::cout << x;  // 4, 16

Coroutines (C++20)

Les coroutines (C++20) permettent les patterns async et générateurs. co_yield suspend et renvoie une valeur. co_await attend une autre coroutine. co_return termine. Le compilateur transforme les coroutines en machines à états. Nécessite une implémentation de type de retour (Generator, Task).

cpp
#include <coroutine>
Generator<int> counter() {
    for (int i = 0; ; ++i)
        co_yield i;  // Suspend and yield
}
for (int x : counter()) {
    if (x > 5) break;
    std::cout << x;
}
// co_await: wait for async operation
// co_return: finish coroutine
27

Pièges courants

Pointeurs pendants

Les pointeurs pendants pointent vers de la mémoire libérée. Y accéder est un comportement indéfini. Utilisez des pointeurs intelligents (unique_ptr, shared_ptr) pour éviter la gestion manuelle de la mémoire. Si vous utilisez des pointeurs bruts, mettez à nullptr après delete. Utilisez des outils comme AddressSanitizer pour détecter.

cpp
// BUG: dangling pointer
int* p = new int(42);
delete p;
std::cout << *p;  // Undefined behavior
// FIX: use smart pointers
auto p = std::make_unique<int>(42);
// Or set to nullptr after delete
int* p2 = new int(42);
delete p2; p2 = nullptr;

Invalidation d'itérateur

Vector push_back peut réallouer, invalidant tous les itérateurs. erase invalide les itérateurs au point et après. Les itérateurs de list sont stables sauf pour les éléments effacés. Vérifiez les règles d'invalidation d'itérateur pour chaque conteneur. En cas de doute, utilisez des indices.

cpp
std::vector<int> v = {1, 2, 3};
// BUG: iterator invalidated
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it == 2) v.push_back(4);  // May invalidate it
}
// FIX: use index
for (size_t i = 0; i < v.size(); ++i) {
    if (v[i] == 2) v.push_back(4);
}

Comportement indéfini

Le comportement indéfini (UB) permet au compilateur de faire n'importe quoi. Les variables non initialisées, l'accès hors bornes, le déréférencement nul, le débordement signé sont tous des UB. Les compilateurs optimisent en supposant pas d'UB, menant à des résultats surprenants. Utilisez -fsanitize=undefined pour détecter.

cpp
int x;  // Uninitialized: UB to read
std::cout << x;  // UB
int arr[5];
arr[5] = 0;  // Out of bounds: UB
int* p = nullptr;
*p;  // Null dereference: UB
// Signed overflow: UB (unsigned wraps)
int big = INT_MAX + 1;  // UB

Slicing

Le slicing d'objet se produit lorsqu'un objet dérivé est copié vers un type de base. Les parties dérivées sont perdues. Passez par référence ou pointeur pour éviter le slicing. Les appels virtuels sur un objet slicé appellent la version de base. Passez toujours les types polymorphes par référence/pointeur.

cpp
class Base { public: virtual ~Base() {} };
class Derived : public Base { int extra; };
void take(Base b) { /* copies Base part only */ }
Derived d;
take(d);  // Sliced: extra lost
// FIX: pass by reference or pointer
void take(const Base& b) { /* full object */ }

Initialisation statique

L'ordre d'initialisation statique entre unités de traduction est indéfini. Un fichier peut utiliser un static d'un autre fichier pas encore initialisé. Corrigez avec des statics locaux à une fonction (Singleton de Meyers) : l'initialisation est paresseuse et thread-safe (C++11+). Accédez via un appel de fonction.

cpp
// Static initialization order fiasco
// File1.cpp
int x = computeX();  // May use y
// File2.cpp
int y = computeY();  // May use x
// Order across files is undefined!
// FIX: function-local static
int& getX() {
    static int x = computeX();  // Lazy, thread-safe
    return x;
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.