C++ C++ Mastery
⚙️ C++ Mastery

C++ — Cours complet et critique

Standards C++17, C++20, C++23. Ce cours est sans concession : on dit ce qui est dangereux, ce qui est dépassé, et ce qu'un C++ moderne attend de toi.

Lis-moi en premier. Ce cours suppose que tu compiles avec un compilateur récent (GCC 11+, Clang 14+, MSVC 2022). Active les warnings : g++ -std=c++20 -Wall -Wextra -Wpedantic -O2. Sans -Wall -Wextra, tu apprends des mauvaises habitudes en silence. C++ ne t'aide pas par défaut — tu dois lui demander.

0.1 — Qu'est-ce que C++ ?

C++ est un langage compilé, fortement typé, multi-paradigme (procédural, orienté objet, générique, fonctionnel). Il descend du C, partage sa philosophie « zéro coût d'abstraction » : tu ne paies que ce que tu utilises, et l'abstraction n'est jamais plus lente que le code écrit à la main.

Trois choses à comprendre tout de suite :

  • Compilation séparée : chaque .cpp devient un fichier objet, puis le linker assemble. Les .h/.hpp sont des déclarations partagées via #include (copier-coller textuel, à l'ancienne).
  • Aucune machine virtuelle : le binaire produit s'exécute nativement. Pas de garbage collector. Tu gères la mémoire (ou tu utilises RAII pour le faire automatiquement).
  • Comportement indéfini (UB) : C++ a des dizaines de cas où le comportement n'est pas défini par le standard. Le compilateur peut faire n'importe quoi. Sortir d'un tableau, déréférencer un pointeur invalide, signed overflow… UB n'est pas « ça crashe parfois », c'est « ton programme n'a aucun sens ».
Les versions du standard. C++98 (vintage), C++11 (révolution moderne : auto, lambdas, move), C++14 (peaufinage), C++17 (structured bindings, optional, variant), C++20 (concepts, ranges, coroutines, modules), C++23 (deducing this, std::expected, multidimensional subscript). Cible C++20 minimum pour du nouveau code aujourd'hui.

0.2 — Le cycle de compilation

Une compilation C++ a quatre phases :

  1. Préprocesseur — applique #include, #define, #ifdef. Produit une seule grosse unité de traduction.
  2. Compilation — vérifie la syntaxe, le typage, génère du code objet (.o).
  3. Édition de liens (linking) — résout les symboles entre fichiers objets et bibliothèques.
  4. Exécution.
# Compilation simple
g++ -std=c++20 -Wall -Wextra -O2 main.cpp -o programme

# Compilation séparée (gros projet)
g++ -std=c++20 -c calc.cpp        # produit calc.o
g++ -std=c++20 -c main.cpp        # produit main.o
g++ calc.o main.o -o programme    # linking

# Mode debug (symboles, pas d'optim)
g++ -std=c++20 -Wall -Wextra -g -O0 main.cpp -o prog_debug
Drapeaux à ne jamais oublier.
  • -Wall -Wextra -Wpedantic : warnings essentiels.
  • -Wshadow -Wconversion -Wsign-conversion : warnings importants ignorés trop souvent.
  • -fsanitize=address,undefined : détecte buffer overflows, UB. Indispensable en dev.
  • -O2 en release. -O0 -g en debug.

0.3 — Hello, World!

#include <iostream>

int main() {
    std::cout << "Bonjour, C++ !\n";
    return 0;
}

Décortiquons :

  • #include <iostream> — directive du préprocesseur. Inclut les flux d'entrée/sortie standard.
  • int main() — point d'entrée. Doit retourner int. Deux signatures valides : int main() et int main(int argc, char* argv[]).
  • std::cout — flux standard de sortie. Le std:: est l'espace de noms standard.
  • << — opérateur d'insertion (surchargé pour les flux).
  • "Bonjour, C++ !\n" — chaîne littérale de type const char*. \n est un saut de ligne.
  • return 0; — convention POSIX : 0 = succès, autre = erreur. main() peut omettre le return, qui vaut alors 0 implicitement (cas unique en C++).
Ne jamais écrire using namespace std; dans un header, et idéalement même pas dans un .cpp de production. Ça pollue l'espace global et casse silencieusement quand un nouveau symbole est ajouté à std::. Préfère using std::cout; ciblé, ou écris simplement std::cout.

1.1 — Types fondamentaux et variables débutant

C++ est statiquement typé : le type de chaque variable est connu à la compilation et ne change pas.

Types entiers

TypeTaille typiquePlage signéeNotes
char1 octet-128 à 127Signé ou non selon la plateforme (!)
short2 octets-32 768 à 32 767
int4 octets±2,1×10⁹Le défaut
long4 ou 8varieÉvite, préfère les types fixes
long long8 octets±9,2×10¹⁸
std::int32_texactement 4fixe<cstdint> — préférable
std::size_tplateformenon signéPour les tailles/index de containers
Critique sévère. Beaucoup de cours apprennent int partout. C'est faux. Pour des tailles de données binaires ou des protocoles, utilise std::int32_t, std::uint64_t, etc. depuis <cstdint>. int n'est pas garanti d'avoir la même taille partout. Et char peut être signé OU non signé : si tu veux des octets, utilise std::uint8_t ou std::byte.

Flottants

  • float — IEEE 754 simple précision, 4 octets, ~7 chiffres significatifs.
  • double — double précision, 8 octets, ~15 chiffres. Le défaut.
  • long double — varie (souvent 8, parfois 10 ou 16). Évite sauf besoin.

Booléen et caractères

  • booltrue/false. Taille 1 octet en pratique.
  • char — caractère ASCII. Pour Unicode, utilise char8_t (UTF-8), char16_t, char32_t, ou wchar_t (déconseillé).

Déclaration et initialisation

int a;              // déclarée mais NON initialisée — UB si lue !
int b = 42;         // initialisation par copie (style C)
int c(42);          // initialisation directe
int d{42};          // initialisation par accolades — RECOMMANDÉ
int e{};            // initialisation par défaut → 0 (zero-init)

int f{3.14};        // ERREUR : narrowing interdit avec {} ✓
int g = 3.14;       // OK silencieusement → 3 (perte d'info, danger !)
Préfère toujours {} pour l'initialisation. Ça interdit les conversions étroites silencieuses (int{3.14} est une erreur, pas un avertissement). C'est l'uniform initialization introduite en C++11. Plus sûre, plus cohérente.

const, constexpr, consteval

const int max_users = 100;            // constante run-time (peut dépendre d'un calcul)
constexpr int kPi100 = 314;            // constante compile-time
constexpr int doubler(int x) { return x * 2; }
int arr[doubler(5)];                  // OK : tableau de 10 (constexpr évalué à la compilation)

consteval int tripler(int x) { return x * 3; }  // C++20 : DOIT être compile-time

auto — déduction de type

auto n = 42;          // int
auto x = 3.14;        // double
auto s = "hello";     // const char* (PAS std::string !)
auto v = std::vector<int>{1, 2, 3};

const auto& ref = v;   // référence constante (évite la copie)

Utilise auto quand le type est évident ou pénible à écrire (itérateurs, lambdas, types templates). Évite-le si ça nuit à la lisibilité.

1.2 — Opérateurs débutant

Arithmétiques

+, -, *, /, % (modulo, entiers uniquement). Division entière : 7 / 2 == 3, pas 3.5. Pour le réel : 7.0 / 2.

Comparaison et logique

==, !=, <, >, <=, >=. C++20 ajoute <=> (spaceship). Logique : &&, ||, !. Court-circuit : a && b n'évalue pas b si a est faux.

Bit à bit

&, |, ^ (xor), ~, <<, >>. Décalage à droite d'un signé négatif est implementation-defined avant C++20.

Affectation composée

+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=.

Incrément/décrément

int i = 5;
int a = ++i;   // pré-incrément : i devient 6, a vaut 6
int b = i++;   // post-incrément : b vaut 6, puis i devient 7
Préfère ++i à i++ quand la valeur retournée n'est pas utilisée — sur des types non triviaux (itérateurs, classes), i++ doit copier l'ancienne valeur.

Ternaire

int max = (a > b) ? a : b;

Précédence

Apprends-en quelques unes (* avant +, && avant ||) et parenthése le reste. a & b == c ne veut pas dire ce que tu crois (== a une précédence plus haute que &).

1.3 — Entrées/sorties basiques

#include <iostream>
#include <string>

int main() {
    std::string nom;
    int age;
    std::cout << "Nom : ";
    std::cin >> nom;
    std::cout << "Âge : ";
    std::cin >> age;
    std::cout << "Salut " << nom << ", " << age << " ans.\n";
}
std::cin >> ne lit qu'un mot (s'arrête au whitespace). Pour lire une ligne entière : std::getline(std::cin, ligne). Mélanger les deux est piégeux : cin >> n laisse le \n dans le buffer, et le getline suivant lira une ligne vide. Solution : std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

Pour écrire avec formatage moderne (C++20), utilise <format> :

#include <format>
std::cout << std::format("x = {}, y = {:.2f}\n", 42, 3.14159);

1.4 — Contrôle de flux débutant

if / else if / else

if (n < 0) {
    std::cout << "négatif\n";
} else if (n == 0) {
    std::cout << "zéro\n";
} else {
    std::cout << "positif\n";
}

// C++17 : if avec init
if (auto it = map.find(key); it != map.end()) {
    use(it->second);
}

switch

switch (jour) {
    case 1: std::cout << "Lundi";     break;
    case 2: std::cout << "Mardi";     break;
    case 3:
    case 4: std::cout << "Milieu";    break;  // fall-through volontaire
    default: std::cout << "Autre";
}
Oublier break est l'erreur classique. C++17 introduit l'attribut [[fallthrough]]; pour documenter qu'un fall-through est volontaire.

switch ne marche que sur des entiers, énumérations ou types convertibles. Pas sur std::string. Pour ça, utilise une chaîne d'if ou un std::unordered_map<std::string, fonction>.

1.5 — Boucles débutant

// while
int i = 0;
while (i < 10) { std::cout << i++; }

// do-while : exécute au moins une fois
do { lire(); } while (!fini);

// for classique
for (int j = 0; j < n; ++j) { ... }

// range-based for (C++11) — préfère ça pour les containers
for (const auto& x : conteneur) { use(x); }

// avec index ET valeur (C++23)
for (auto [i, v] : std::views::enumerate(vec)) { ... }
Critique majeure. Trop de devs écrivent encore for (int i = 0; i < vec.size(); ++i). Trois problèmes :
  1. vec.size() retourne std::size_t (non signé) — comparaison signée/non-signée → warning.
  2. Le compilateur ré-évalue vec.size() à chaque itération (sauf si optim).
  3. Tu n'as pas besoin de l'index 90% du temps. Utilise for (auto& x : vec).
Si tu as vraiment besoin de l'index : for (std::size_t i = 0; i < vec.size(); ++i).

break, continue

break sort de la boucle. continue passe à l'itération suivante. Pas de goto ni de break étiqueté en C++ (échappes de boucles imbriquées avec une fonction et return, ou un flag).

1.6 — Fonctions débutant

int somme(int a, int b) {
    return a + b;
}

// Paramètres par défaut (du plus à droite vers le plus à gauche)
void log(const std::string& msg, int level = 0);

// Surcharge : même nom, signatures différentes
int    aire(int cote);
double aire(double rayon);
int    aire(int l, int h);

// Passage : par valeur, par référence, par référence const
void copie(std::string s);              // copie complète (cher)
void modifier(std::string& s);          // modifie l'original
void lire_seul(const std::string& s);   // pas de copie, pas de modif — défaut

// Trailing return type (utile avec auto/templates)
auto multiplier(double a, double b) -> double { return a * b; }

Récursivité

int fact(int n) {
    return (n <= 1) ? 1 : n * fact(n - 1);
}
Récursivité en C++ : danger. Pas d'optimisation tail-call garantie. Une récursion profonde explose la pile (stack overflow). Pour des problèmes itératifs, utilise une boucle. La récursion brille pour les structures intrinsèquement récursives (arbres, graphes).

inline, static, noexcept, [[nodiscard]]

inline int add(int a, int b) { return a + b; }
// inline = autorise plusieurs définitions identiques (utile en header).
//          NE force PAS l'inlining (le compilo décide).

static int helper() { ... }   // fonction privée à l'unité de traduction (.cpp)

int divise(int a, int b) noexcept;  // promet de ne pas lever d'exception

[[nodiscard]] int calculer();  // le compilo prévient si la valeur est ignorée

1.7 — Portée et durée de vie débutant

Une variable a une portée (où elle est visible) et une durée de vie (quand elle existe en mémoire).

  • Locale : déclarée dans une fonction/bloc, vit jusqu'à la } fermante. Allouée sur la pile.
  • Globale : déclarée hors de toute fonction. Vit toute l'exécution. À éviter.
  • Statique locale : static int counter = 0; dans une fonction — initialisée une fois, persiste entre les appels.
  • Membre de classe : durée de vie liée à l'objet.
  • Allouée dynamiquement : new/malloc — durée de vie contrôlée explicitement.
int compteur() {
    static int n = 0;     // initialisé UNE FOIS, persiste
    return ++n;
}
// Appels successifs : 1, 2, 3, ...
Ne retourne JAMAIS de référence/pointeur vers une variable locale.
int& danger() {
    int x = 42;
    return x;   // UB : x meurt à la fin de la fonction
}
Le compilateur peut prévenir, ou pas. À l'exécution, tu lis de la mémoire détruite.

2.1 — Tableaux et chaînes C débutant

int notes[5] = {12, 15, 8, 17, 11};
notes[0] = 10;
std::cout << notes[3];

// Taille avec sizeof (uniquement quand le tableau n'a pas "decay" en pointeur)
std::size_t n = sizeof(notes) / sizeof(notes[0]);

// Multi-dimensionnel
int mat[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};

Chaînes C : char[] et const char*

const char* s = "hello";       // pointeur vers chaîne littérale (zéro-terminée)
char buf[100];                 // tampon
std::strcpy(buf, s);            // fonctions C : <cstring>
std::cout << std::strlen(s);   // 5 (sans le \0)
Critique. Les tableaux C et les chaînes C sont une plaie. Pas de bornes vérifiées, taille perdue dès qu'on les passe à une fonction (« pointer decay »), API confuses. En C++ moderne, utilise std::array<T, N> (taille fixe), std::vector<T> (dynamique), et std::string (texte). Les tableaux C ne servent que pour interfacer avec des API C ou pour des micro-optimisations très spécifiques.
#include <array>
std::array<int, 5> notes = {12, 15, 8, 17, 11};
notes.size();         // connue à la compilation
notes.at(10);         // lance std::out_of_range si hors bornes

2.2 — Pointeurs intermédiaire

Un pointeur stocke une adresse mémoire. L'astérisque appartient au type mentalement, mais syntaxiquement il s'attache à la variable :

int x = 42;
int* p = &x;          // p contient l'adresse de x
std::cout << *p;       // déréférencement → 42
*p = 100;              // modifie x via p → x vaut 100

int* p1, p2;          // PIÈGE : p1 est int*, p2 est int. Évite cette syntaxe.
int *p1, *p2;         // les deux sont int* (au moins explicite)

// nullptr (C++11) : pointeur "vide" typé. PAS NULL ni 0.
int* q = nullptr;
if (q) { ... }        // teste s'il pointe sur quelque chose

Arithmétique des pointeurs

int arr[] = {10, 20, 30, 40};
int* p = arr;         // pointe sur arr[0]
std::cout << *(p + 2); // 30 — équivalent à arr[2]
++p;                   // p pointe sur arr[1] (avance de sizeof(int))

Pointeurs constants

const int* p1;        // pointeur vers int const  : *p1 = 5 ❌ ; p1 = autre ✓
int const* p2;        // IDENTIQUE à p1 (lecture droite-à-gauche)
int* const p3 = &x;   // pointeur const vers int : *p3 = 5 ✓ ; p3 = autre ❌
const int* const p4 = &x;  // les deux sont const
Astuce de lecture. Lis de droite à gauche en remplaçant * par "pointeur vers". const int* const = "const pointeur vers const int".
Pointeurs : où ça merde.
  • Pointeur non initialisé = pointe n'importe où. UB en lecture.
  • Pointeur dangling = pointe sur de la mémoire libérée. UB.
  • Double-free = libérer deux fois. UB.
  • Memory leak = oublier delete. Pas UB, mais fuite.
La solution C++ moderne : n'utilise quasi jamais de pointeur brut propriétaire. Utilise std::unique_ptr et std::shared_ptr. Les pointeurs bruts ne servent qu'à observer sans posséder.

2.3 — Références intermédiaire

Une référence est un alias pour une variable existante. Pas une variable séparée, pas un pointeur déguisé : c'est l'objet lui-même, vu sous un autre nom.

int x = 42;
int& r = x;     // r EST x
r = 100;         // x vaut maintenant 100
&r == &x;       // true

Règles importantes

  • Une référence doit être initialisée à sa création.
  • Une référence ne peut jamais être réaffectée (à un autre objet).
  • Pas de référence vers rien : pas de "null reference" en C++.

Référence constante

const std::string& name() const;   // retour par const-ref : pas de copie
void log(const std::string& msg);  // passage en lecture sans copie
Référence vs pointeur.
  • Référence : ne peut pas être null, ne peut pas être réassignée, syntaxe identique à la variable. Utilise pour les paramètres de fonction et quand l'objet doit toujours exister.
  • Pointeur : peut être null, peut pointer ailleurs, peut être stocké dans des containers. Utilise quand l'absence est valide ou pour la mémoire dynamique.

2.4 — Allocation dynamique intermédiaire

int* p = new int(42);     // alloue sur le tas, valeur 42
delete p;                  // libère
p = nullptr;               // bonne pratique post-delete

int* arr = new int[100];   // tableau dynamique
delete[] arr;              // ATTENTION : delete[] pour les tableaux
L'erreur la plus chère du C++.
  • new sans delete → fuite.
  • delete deux fois → corruption.
  • new[] avec delete (sans []) → UB.
  • Exception entre new et delete → fuite si pas de RAII.
En C++ moderne, tu n'écris JAMAIS new ni delete à la main. Tu utilises :
  • std::make_unique<T>(args...) pour la possession unique
  • std::make_shared<T>(args...) pour la possession partagée
  • std::vector<T> pour les tableaux dynamiques
#include <memory>
auto p = std::make_unique<int>(42);  // libéré automatiquement
auto arr = std::make_unique<int[]>(100);  // tableau dynamique sûr

2.5 — Structures, unions et énumérations

struct

struct Point {
    double x;
    double y;
};

Point p{3.0, 4.0};       // aggregate initialization
std::cout << p.x;

// C++20 : designated initializers
Point q{.x = 1.0, .y = 2.0};

En C++, struct et class sont quasiment identiques : la seule différence est que les membres d'un struct sont publics par défaut, ceux d'une class sont privés. Convention : struct pour les types-données passifs, class dès qu'il y a des invariants.

enum class (C++11)

enum class Couleur { Rouge, Vert, Bleu };
Couleur c = Couleur::Rouge;

// PAS d'enum non-class :
//   enum Couleur { ROUGE, VERT, BLEU };  // ROUGE pollue le scope ! Évite.
Toujours préférer enum class aux enum classiques. Pas de pollution de scope, pas de conversions implicites en int, fortement typé.

3.1 — Classes intermédiaire

class Cercle {
public:
    Cercle(double r) : rayon_{r} {}     // constructeur

    double aire() const { return 3.14159 * rayon_ * rayon_; }
    void   set_rayon(double r) { rayon_ = r; }

private:
    double rayon_;
};

Cercle c{5.0};
std::cout << c.aire();

Anatomie

  • Membres : variables (état) et fonctions (comportement).
  • Spécificateurs d'accès : public, protected, private.
  • Constructeur : initialise. Destructeur : nettoie.
  • const à la fin d'une méthode : promet de ne pas modifier l'objet. Très important.
  • this : pointeur implicite vers l'objet courant. Utilise-le rarement explicitement.
Convention de nommage. Plusieurs styles existent : m_rayon, rayon_, this->rayon… Choisis-en un et reste cohérent. rayon_ (suffixe underscore) est le plus utilisé en C++ moderne.

3.2 — Constructeurs et destructeurs intermédiaire

Liste d'initialisation

class Personne {
    std::string nom_;
    int age_;
public:
    Personne(std::string n, int a)
        : nom_{std::move(n)}, age_{a} {}   // liste d'init : ORDRE = ordre de déclaration
};
L'ordre d'initialisation suit l'ordre de déclaration des membres dans la classe, pas l'ordre dans la liste d'init. Si tu écris : age_{n}, nom_{a} mais que nom_ est déclaré avant, c'est nom_ qui s'initialise en premier. Active -Wreorder pour les warnings.

Les "Big Three / Five / Zero"

Si ta classe gère une ressource, le compilateur génère des opérations par défaut. Si elles sont incorrectes, tu dois en définir au moins 5 :

class Resource {
public:
    Resource();                                // constructeur par défaut
    ~Resource();                               // destructeur
    Resource(const Resource& other);          // constructeur de copie
    Resource& operator=(const Resource& other);  // affectation par copie
    Resource(Resource&& other) noexcept;        // constructeur de déplacement (C++11)
    Resource& operator=(Resource&& other) noexcept;  // affectation par déplacement
};
Règle des 0 / 5. Soit ta classe ne gère AUCUNE ressource (et tu ne définis rien : règle des 0), soit elle en gère et tu définis les 5. La règle des 0 est le but : compose ta classe avec des types qui gèrent eux-mêmes leur mémoire (std::string, std::vector, std::unique_ptr) et le compilateur fait le reste correctement.

Constructeurs spéciaux

class Foo {
public:
    Foo() = default;                // utilise le défaut généré
    Foo(const Foo&) = delete;       // interdit la copie
    Foo& operator=(const Foo&) = delete;

    explicit Foo(int x);            // interdit la conversion implicite int → Foo
};

// Constructeur de délégation (C++11)
class Bar {
    int a_, b_;
public:
    Bar(int a, int b) : a_{a}, b_{b} {}
    Bar() : Bar(0, 0) {}              // délègue à l'autre constructeur
};
Toujours explicit pour les constructeurs à un seul paramètre, sauf si tu veux la conversion implicite (rare). Sinon : void f(MaClasse) {} ; f(42); peut compiler de manière inattendue.

3.3 — Encapsulation

Encapsuler = cacher l'état interne, exposer un comportement. Pas juste mettre des private et coller des get_x() / set_x() partout — ça, c'est un struct déguisé.

Critique fondamentale. Une classe avec un getter/setter pour chaque membre est un struct mal écrit. La bonne question : quelle abstraction ta classe modélise ? Si Compte a get_solde() et set_solde(), c'est faux. Il devrait avoir solde(), deposer(montant), retirer(montant). Le setter naïf casse les invariants — par exemple, un solde ne devrait pas pouvoir devenir négatif sans déclencher une logique métier.
class Compte {
    double solde_ = 0;
    std::string proprietaire_;
public:
    Compte(std::string proprietaire, double solde_initial = 0);

    double solde() const { return solde_; }
    const std::string& proprietaire() const { return proprietaire_; }

    void deposer(double montant);
    bool retirer(double montant);  // retourne false si fonds insuffisants
};

3.4 — Surcharge d'opérateurs intermédiaire

class Vec2 {
    double x_, y_;
public:
    Vec2(double x = 0, double y = 0) : x_{x}, y_{y} {}

    Vec2 operator+(const Vec2& o) const { return {x_ + o.x_, y_ + o.y_}; }
    Vec2& operator+=(const Vec2& o) { x_ += o.x_; y_ += o.y_; return *this; }

    bool operator==(const Vec2& o) const { return x_ == o.x_ && y_ == o.y_; }

    double x() const { return x_; }
    double y() const { return y_; }
};

// Stream operator : doit être libre (non-membre)
std::ostream& operator<<(std::ostream& os, const Vec2& v) {
    return os << "(" << v.x() << ", " << v.y() << ")";
}

// C++20 : spaceship operator → génère ==, !=, <, <=, >, >= en une ligne
class Date {
    int annee_, mois_, jour_;
public:
    auto operator<=>(const Date&) const = default;
};
Bonnes pratiques surcharge.
  • Surcharge += en membre, puis + en libre en termes de +=.
  • Comparateurs et stream operators en non-membres.
  • Ne surcharge pas &&, ||, , — tu casses la sémantique de court-circuit.
  • Surcharge de operator-> uniquement dans des wrappers (smart pointers, iterators).

3.5 — Héritage intermédiaire

class Animal {
public:
    Animal(std::string nom) : nom_{std::move(nom)} {}
    virtual ~Animal() = default;     // destructeur virtuel : INDISPENSABLE

    virtual std::string crier() const = 0;   // méthode virtuelle pure

    const std::string& nom() const { return nom_; }
protected:
    std::string nom_;
};

class Chien : public Animal {
public:
    Chien(std::string nom) : Animal{std::move(nom)} {}
    std::string crier() const override { return "Wouaf !"; }
};

class Chat : public Animal {
public:
    Chat(std::string nom) : Animal{std::move(nom)} {}
    std::string crier() const override { return "Miaou"; }
};

Modes d'héritage

Modepublic dans baseprotected dans baseprivate dans base
: public Basepublicprotectedinaccessible
: protected Baseprotectedprotectedinaccessible
: private Baseprivateprivateinaccessible

En pratique : héritage public 99% du temps (relation "est-un"). protected et private sont rares et expriment "implémenté en termes de" — préfère la composition.

Le destructeur virtuel. Si une classe a au moins une méthode virtuelle, son destructeur DOIT être virtuel. Sinon, delete pAnimalpAnimal pointe sur un Chien ne détruit que la partie Animal → fuite mémoire et UB.

3.6 — Polymorphisme intermédiaire

std::vector<std::unique_ptr<Animal>> zoo;
zoo.push_back(std::make_unique<Chien>("Rex"));
zoo.push_back(std::make_unique<Chat>("Felix"));

for (const auto& a : zoo) {
    std::cout << a->nom() << " : " << a->crier() << "\n";
    // Appel virtuel : sélectionne crier() selon le type dynamique
}

Mots-clés essentiels

  • virtual — déclare une méthode polymorphe.
  • override — promet de surcharger une méthode virtuelle. Erreur de compilation si pas le cas. Toujours utiliser.
  • final — interdit de surcharger plus loin.
  • = 0 — méthode virtuelle pure → classe abstraite.

Slicing : le piège classique

void parle(Animal a) {     // PAR VALEUR — danger !
    std::cout << a.crier();
}
Chien c{"Rex"};
parle(c);   // La copie d'un Chien dans un Animal "tranche" le Chien.
            //   Seule la partie Animal est copiée.
Slicing. Polymorphisme requiert référence ou pointeur. Passer par valeur perd les méthodes spécifiques. Toujours void parle(const Animal&) ou void parle(Animal*).

3.7 — Classes abstraites et interfaces

Une classe est abstraite si elle a au moins une méthode virtuelle pure. On ne peut pas l'instancier.

class Forme {
public:
    virtual ~Forme() = default;
    virtual double aire() const = 0;
    virtual double perimetre() const = 0;

    // Méthode normale qui utilise les virtuelles
    double ratio() const { return aire() / perimetre(); }
};

Une interface pure en C++ est une classe abstraite avec uniquement des méthodes virtuelles pures (et un destructeur virtuel). Pas de mot-clé interface.

3.8 — Héritage multiple et virtual avancé

class A { public: int a; };
class B : public A { public: int b; };
class C : public A { public: int c; };
class D : public B, public C { };
// D contient DEUX A : B::a et C::a — diamant.

// Solution : héritage virtuel
class B : virtual public A { ... };
class C : virtual public A { ... };
class D : public B, public C { };  // un seul A
L'héritage multiple est un piège. Sauf pour des cas légitimes (mixin de comportements via interfaces pures), évite. Préfère composition + interfaces. Le diamant virtuel a un coût d'exécution non nul (vptr supplémentaire) et complique massivement l'initialisation (la classe la plus dérivée doit appeler le constructeur de la base virtuelle).

4.1 — std::string intermédiaire

#include <string>

std::string s = "hello";
s += " world";
s.size();              // 11
s.length();            // idem
s.empty();             // false
s[0];                  // 'h' — pas de check
s.at(0);               // 'h' — lance si hors-bornes
s.front(); s.back();
s.substr(6, 5);          // "world"
s.find("world");        // 6 (ou std::string::npos si introuvable)
s.replace(6, 5, "there");
s.starts_with("hello");  // C++20
s.ends_with("there");    // C++20
s.contains("the");       // C++23

// Conversion
std::string n = std::to_string(42);
int i = std::stoi("42");
double d = std::stod("3.14");

std::string_view (C++17)

#include <string_view>

void log(std::string_view msg) {  // pas de copie, pas d'allocation
    std::cout << msg;
}
log("littéral");                  // OK
log(std::string{"hello"});       // OK
log("hello world".substr(0, 5)); // OK
string_view est une référence — il ne possède pas la mémoire. Ne le stocke pas si la chaîne sous-jacente peut mourir. Ne le retourne JAMAIS depuis une fonction qui possède la chaîne (UB).

4.2 — Containers de la STL intermédiaire

ContainerCaractéristiqueAccèsInsertionQuand utiliser
vectorTableau dynamique contiguO(1)fin: O(1) amortiLe défaut. Toujours commencer par ça.
arrayTableau de taille fixeO(1)Taille connue à la compilation
dequeDouble-ended queueO(1)début/fin: O(1)Files; insertions aux deux bouts
listListe doublement chaînéeO(n)O(1) avec iteratorTrès rare. Mauvaise localité cache.
forward_listListe simplement chaînéeO(n)O(1)Encore plus rare
mapArbre rouge-noir triéO(log n)O(log n)Clés ordonnées
unordered_mapTable de hachageO(1) moy.O(1) moy.Map par défaut si l'ordre n'importe pas
set / unordered_setComme map mais sans valeurUnicité, appartenance
stack / queue / priority_queueAdapteursSémantique LIFO/FIFO/heap

vector en détail

#include <vector>

std::vector<int> v;
v.push_back(10);
v.emplace_back(20);            // construit en place — préfère pour les types non triviaux
v.size();
v.capacity();                  // peut être > size
v.reserve(1000);                // pré-alloue : évite les ré-allocations
v.shrink_to_fit();             // libère la capacité non utilisée

v[0];                          // pas de check
v.at(0);                       // check, lance si hors-bornes
v.front(); v.back();
v.begin(); v.end();            // itérateurs

v.insert(v.begin() + 2, 99);   // O(n) : décale tout à droite
v.erase(v.begin() + 2);        // O(n)
v.clear();

// Initialisation
std::vector<int> a{1, 2, 3};      // liste d'init
std::vector<int> b(10, 0);      // 10 zéros
std::vector<int> c(10);          // 10 valeurs par défaut (0 pour int)
Invalidation d'itérateurs. Insérer dans un vector peut invalider tous les itérateurs/pointeurs/références aux éléments si la capacité est dépassée. Les autres containers ont des règles différentes ; lis la doc avant de stocker des références à long terme.

map et unordered_map

#include <unordered_map>

std::unordered_map<std::string, int> ages;
ages["Alice"] = 30;
ages.insert({"Bob", 25});
ages.emplace("Carol", 40);

if (auto it = ages.find("Alice"); it != ages.end()) {
    std::cout << it->first << " → " << it->second;
}
if (ages.contains("Dan")) { ... }   // C++20

for (const auto& [nom, age] : ages) {  // structured binding C++17
    std::cout << nom << ": " << age << "\n";
}
Piège : map[key] insère une entrée par défaut si la clé n'existe pas. Pour juste lire, utilise at() (lance si absent) ou find().

4.3 — Itérateurs

Un itérateur est une généralisation du pointeur. Il y a 6 catégories (en C++20 : 6 concepts) :

  1. InputIterator — lecture, avance (++).
  2. OutputIterator — écriture, avance.
  3. ForwardIterator — multi-passe, lit/écrit.
  4. BidirectionalIterator-- en plus.
  5. RandomAccessIterator+n, -n, [n], <.
  6. ContiguousIterator (C++20) — RandomAccess + mémoire contiguë (vector, array, string).
std::vector<int> v{1, 2, 3, 4, 5};

for (auto it = v.begin(); it != v.end(); ++it) {
    std::cout << *it;
}

// Itérateur inverse
for (auto it = v.rbegin(); it != v.rend(); ++it) {
    std::cout << *it;   // 5 4 3 2 1
}

// const-iterator
for (auto it = v.cbegin(); it != v.cend(); ++it) {
    // *it est const
}

// std::advance, std::distance, std::next, std::prev
auto it = v.begin();
std::advance(it, 3);            // avance de 3
auto j = std::next(it);          // it+1 sans modifier it
auto n = std::distance(v.begin(), it);  // 3

4.4 — Algorithms intermédiaire

#include <algorithm>
#include <numeric>

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

// Tri
std::sort(v.begin(), v.end());                       // croissant
std::sort(v.begin(), v.end(), std::greater<>{});     // décroissant
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });

// Recherche
auto it = std::find(v.begin(), v.end(), 4);
bool ok = std::binary_search(v.begin(), v.end(), 5);  // requiert tri

// Comptage et accumulation
auto n = std::count(v.begin(), v.end(), 1);          // 2
auto s = std::accumulate(v.begin(), v.end(), 0);      // somme
auto p = std::accumulate(v.begin(), v.end(), 1, std::multiplies<>{});

// Transformation
std::vector<int> out(v.size());
std::transform(v.begin(), v.end(), out.begin(), [](int x) { return x * x; });

// Filtrage : copy_if
std::vector<int> pairs;
std::copy_if(v.begin(), v.end(), std::back_inserter(pairs),
             [](int x) { return x % 2 == 0; });

// Min/max/extremums
auto [mn, mx] = std::minmax_element(v.begin(), v.end());

// any_of, all_of, none_of
bool a = std::any_of(v.begin(), v.end(), [](int x) { return x > 10; });

// Modification : remove_if + erase (idiome erase-remove)
v.erase(std::remove_if(v.begin(), v.end(), [](int x) { return x < 3; }),
        v.end());
// C++20 : std::erase_if(v, predicat) en une ligne
"No raw loops". Sean Parent (Adobe) en 2013 : si tu écris une boucle, demande-toi s'il existe un algo. std::sort, std::find, std::transform sont mieux testés, plus expressifs, parfois plus rapides. Avec C++20 ranges, c'est encore plus joli : std::ranges::sort(v).

4.5 — Exceptions intermédiaire

#include <stdexcept>

double divise(double a, double b) {
    if (b == 0) throw std::invalid_argument("division par zéro");
    return a / b;
}

try {
    double r = divise(10, 0);
} catch (const std::invalid_argument& e) {
    std::cerr << "Erreur arg : " << e.what();
} catch (const std::exception& e) {
    std::cerr << "Erreur : " << e.what();
} catch (...) {
    std::cerr << "Inconnue";
}

Hiérarchie standard (extrait)

std::exception
├── std::logic_error
│   ├── std::invalid_argument
│   ├── std::domain_error
│   ├── std::length_error
│   └── std::out_of_range
└── std::runtime_error
    ├── std::range_error
    ├── std::overflow_error
    └── std::underflow_error
Bonnes pratiques exceptions.
  • Lance par valeur, attrape par const-référence. throw MyException{...}; et catch (const MyException& e).
  • Hérite de std::exception ou d'une de ses dérivées.
  • Les destructeurs ne doivent jamais laisser une exception s'échapper (UB si une exception est déjà active).
  • Marque les fonctions sans exception noexcept — c'est une optimisation et une garantie.
  • Une fonction qui ne peut pas tenir ses garanties en cas d'exception doit avoir au minimum la basic guarantee (état valide). Idéalement strong guarantee (rollback : commit ou rien) ou nothrow.
Critique : exceptions vs codes d'erreur. Le débat fait rage. Les exceptions sont gratuites sur le chemin heureux mais coûtent cher quand elles sont lancées. Pour des erreurs attendues (clé absente, parsing échoué), préfère std::optional, std::expected (C++23), ou un retour tl::expected. Les exceptions sont pour des conditions exceptionnelles.

4.6 — I/O fichiers

#include <fstream>
#include <sstream>

// Écriture
std::ofstream out("sortie.txt");
if (!out) throw std::runtime_error("impossible d'ouvrir");
out << "Ligne 1\n";

// Lecture ligne par ligne
std::ifstream in("entree.txt");
std::string ligne;
while (std::getline(in, ligne)) {
    std::cout << ligne << "\n";
}

// Lecture mot par mot, ou typée
int n;
double x;
in >> n >> x;

// stringstream : I/O en mémoire
std::stringstream ss;
ss << "42 3.14 hello";
int i; double d; std::string s;
ss >> i >> d >> s;

// Mode binaire
std::ofstream bin("data.bin", std::ios::binary);
int v = 42;
bin.write(reinterpret_cast<const char*>(&v), sizeof(v));

Les flux ferment automatiquement à la destruction (RAII). Pas besoin d'appeler close() sauf besoin spécifique.

5.1 — auto, range-for, nullptr (C++11)

auto v = std::vector<int>{1, 2, 3};
for (const auto& x : v) std::cout << x;

int* p = nullptr;       // PAS NULL ni 0 (qui sont int)

// auto& vs auto vs auto&&
for (auto x : v)        // COPIE chaque élément
for (auto& x : v)       // référence — peut modifier
for (const auto& x : v) // référence const — DÉFAUT pour la lecture
for (auto&& x : v)      // universal ref — pour cas génériques

5.2 — Smart pointers avancé

#include <memory>

// unique_ptr : possession unique, déplaçable mais pas copiable
auto p = std::make_unique<Foo>(42);
auto q = std::move(p);    // p devient nul
// auto r = p; ❌ ne compile pas

// shared_ptr : possession partagée (compteur de références)
auto s1 = std::make_shared<Foo>(42);
auto s2 = s1;             // compteur = 2
s1.use_count();           // 2

// weak_ptr : référence non-possédante (casse les cycles)
std::weak_ptr<Foo> w = s1;
if (auto locked = w.lock()) {
    locked->methode();
}

Quand utiliser quoi

  • unique_ptr — 95% du temps. Pas de coût d'exécution. Possession claire.
  • shared_ptr — quand plusieurs objets co-possèdent. Coût : compteur atomique.
  • weak_ptr — accompagne shared_ptr pour casser des cycles (ex: parent → enfant en shared, enfant → parent en weak).
Les pièges des smart pointers.
  • Cycles avec shared_ptr → fuite. Utilise weak_ptr sur le côté "non-propriétaire" du cycle.
  • Construire un shared_ptr à partir d'un raw pointer crée un nouveau compteur : shared_ptr<T>{ptr_brut} deux fois → double-free. Toujours make_shared.
  • Passer shared_ptr par valeur copie le compteur (atomique). Préfère const shared_ptr& ou même un T* brut quand l'API n'a pas besoin de partager la possession.

5.3 — Move semantics et rvalue references avancé

Une rvalue reference (T&&) capture une valeur temporaire. Cela permet de "voler" ses ressources au lieu de les copier.

std::string a = "hello";
std::string b = a;                  // COPIE : alloue, copie les caractères
std::string c = std::move(a);       // MOVE : transfère, a devient vide (mais valide !)

// std::move ne déplace PAS — c'est juste un cast vers T&&.
//   Le déplacement est réalisé par le constructeur de move de la classe.

Implémenter move

class Buffer {
    int* data_;
    std::size_t size_;
public:
    Buffer(std::size_t n) : data_{new int[n]}, size_{n} {}
    ~Buffer() { delete[] data_; }

    // Move constructor
    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;
    }
};
Règles d'or.
  • Marque move ops noexcept. Sinon std::vector les ignore et préfère copier (pour la garantie forte).
  • Un objet déplacé reste valide mais "vide" : tu peux le détruire ou lui réassigner, mais pas l'utiliser autrement.
  • La règle des 5 : si tu écris l'un parmi destructeur, copy ctor, copy assign, move ctor, move assign, écris-les tous (ou défie le défaut explicitement avec = default / = delete).
  • Préfère la règle des 0 : compose avec des types qui font le sale boulot.

Perfect forwarding

template <typename T>
void wrapper(T&& arg) {
    callee(std::forward<T>(arg));   // préserve la catégorie de valeur
}

T&& dans un template est une universal reference (forwarding reference) : se comporte comme T& ou T&& selon ce qui est passé. std::forward<T> préserve cette catégorie.

5.4 — Lambdas avancé

// Syntaxe : [capture](paramètres) -> type_retour { corps }
auto add = [](int a, int b) { return a + b; };
add(3, 4);  // 7

int seuil = 10;
auto au_dessus = [seuil](int x) { return x > seuil; };  // capture par valeur

int total = 0;
std::for_each(v.begin(), v.end(), [&total](int x) { total += x; });  // par référence

// Captures :
[]      // rien
[=]     // tout par valeur (DÉCONSEILLÉ : capture excessive)
[&]     // tout par référence (DÉCONSEILLÉ)
[x]     // x par valeur
[&x]    // x par référence
[x = expr]              // init capture (C++14)
[ptr = std::move(p)]    // move capture
[this]   // capture le this courant (membres accessibles)

// Lambda générique (C++14)
auto add = [](auto a, auto b) { return a + b; };

// Lambda template (C++20)
auto f = []<typename T>(T x) { return x * x; };

// mutable : permet de modifier les captures par valeur
auto compteur = [n = 0]() mutable { return ++n; };
Capture par référence et durée de vie. Une lambda qui survit au scope de capture devient un trou de sécurité.
auto make_lambda() {
    int x = 42;
    return [&x]() { return x; };  // UB : x mort à l'appel
}

5.5 — RAII : Resource Acquisition Is Initialization

RAII est l'idiome central de C++. Une ressource (mémoire, fichier, mutex, socket…) est liée à la durée de vie d'un objet : acquise au constructeur, libérée au destructeur. Conséquence : pas de fuite, même en cas d'exception.

class FileHandle {
    std::FILE* f_;
public:
    FileHandle(const char* path, const char* mode)
        : f_{std::fopen(path, mode)}
    {
        if (!f_) throw std::runtime_error("open failed");
    }
    ~FileHandle() { if (f_) std::fclose(f_); }

    FileHandle(const FileHandle&) = delete;          // non-copiable
    FileHandle& operator=(const FileHandle&) = delete;

    FileHandle(FileHandle&& o) noexcept : f_{o.f_} { o.f_ = nullptr; }
    FileHandle& operator=(FileHandle&& o) noexcept {
        if (this != &o) {
            if (f_) std::fclose(f_);
            f_ = o.f_;
            o.f_ = nullptr;
        }
        return *this;
    }

    std::FILE* get() const { return f_; }
};
RAII en une phrase. Si tu écris du code qui acquiert quelque chose qui doit être libéré, et que cette gestion n'est pas dans un destructeur, tu écris du code C++ des années 90.

5.6 — constexpr, consteval, constinit avancé

constexpr int factorielle(int n) {
    return (n <= 1) ? 1 : n * factorielle(n - 1);
}

constexpr int v = factorielle(5);    // 120, calculé à la compilation
int n;
std::cin >> n;
int r = factorielle(n);             // OK : aussi à l'exécution

consteval int tripler(int x) { return x * 3; }  // C++20 : DOIT être compile-time
int a = tripler(5);                 // OK : 5 est constexpr
int b = tripler(n);                 // ERREUR : n n'est pas connu à la compilation

constinit std::string g = "hello";  // init à la compil mais modifiable runtime

Depuis C++20, beaucoup de la STL est constexpr : std::vector, std::string, algorithmes… Tu peux écrire des programmes entiers qui s'exécutent à la compilation.

6.1 — Templates de fonctions avancé

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

max(3, 5);              // T = int
max<double>(3, 5.5);   // T = double explicite
max("a", "b");          // T = const char* — compare des pointeurs ! 🚨

Spécialisation

template <>
const char* max<const char*>(const char* a, const char* b) {
    return std::strcmp(a, b) > 0 ? a : b;
}

auto, decltype, decltype(auto)

template <typename A, typename B>
auto somme(A a, B b) -> decltype(a + b) {
    return a + b;
}

// C++14 : juste auto suffit
template <typename A, typename B>
auto somme(A a, B b) { return a + b; }

6.2 — Templates de classes

template <typename T>
class Stack {
    std::vector<T> data_;
public:
    void push(const T& x) { data_.push_back(x); }
    void push(T&& x) { data_.push_back(std::move(x)); }

    T pop() {
        if (data_.empty()) throw std::runtime_error("empty");
        T v = std::move(data_.back());
        data_.pop_back();
        return v;
    }

    bool   empty() const { return data_.empty(); }
    std::size_t size() const { return data_.size(); }
};

Stack<int> s;
s.push(42);

Template avec valeurs non-types

template <typename T, std::size_t N>
class FixedArray {
    T data_[N];
public:
    T& operator[](std::size_t i) { return data_[i]; }
    std::size_t size() const { return N; }
};
FixedArray<int, 10> arr;
Templates et compilation. Le code d'un template doit être visible (typiquement dans le header) — il est instancié là où il est utilisé. C'est pour ça qu'on ne sépare pas template .h/.cpp normalement. C++20 modules améliorent ça.

6.3 — Variadic templates et fold expressions expert

// Variadic template (C++11)
template <typename... Args>
void print_all(Args&&... args) {
    ((std::cout << args << ' '), ...);   // fold expression (C++17)
    std::cout << '\n';
}
print_all(1, 2.5, "hello", 'x');

// Somme variadique
template <typename... Args>
auto somme(Args... args) {
    return (args + ...);     // fold à droite
}
somme(1, 2, 3, 4);   // 10

Récursion classique (avant C++17)

template <typename T>
void print(T&& x) { std::cout << x << '\n'; }

template <typename T, typename... Rest>
void print(T&& first, Rest&&... rest) {
    std::cout << first << ' ';
    print(std::forward<Rest>(rest)...);
}

6.4 — SFINAE et type traits expert

SFINAE = "Substitution Failure Is Not An Error". Si la substitution d'un template échoue, le compilateur l'ignore au lieu d'émettre une erreur.

#include <type_traits>

// std::enable_if
template <typename T,
          typename = std::enable_if_t<std::is_integral_v<T>>>
void f(T x) { /* uniquement pour les entiers */ }

// if constexpr (C++17) — souvent plus simple que SFINAE
template <typename T>
void describe(T x) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << "entier : " << x;
    } else if constexpr (std::is_floating_point_v<T>) {
        std::cout << "flottant : " << x;
    } else {
        std::cout << "autre";
    }
}

Type traits utiles dans <type_traits> :

  • is_integral_v<T>, is_floating_point_v<T>
  • is_same_v<T, U>
  • is_base_of_v<Base, Derived>
  • is_pointer_v<T>, is_reference_v<T>
  • remove_const_t<T>, remove_reference_t<T>, decay_t<T>
  • conditional_t<C, T1, T2>
Critique. SFINAE était la torture du C++ moderne avant 2020. Avec C++20 concepts, on devrait l'oublier. Si tu écris du nouveau code C++20+, utilise requires et concepts au lieu de enable_if. SFINAE reste utile pour comprendre du legacy.

6.5 — CRTP : Curiously Recurring Template Pattern expert

template <typename Derived>
class Comparable {
public:
    bool operator!=(const Derived& o) const {
        return !(static_cast<const Derived&>(*this) == o);
    }
    bool operator>(const Derived& o) const {
        return o < static_cast<const Derived&>(*this);
    }
    // etc.
};

class Money : public Comparable<Money> {
    int cents_;
public:
    bool operator==(const Money& o) const { return cents_ == o.cents_; }
    bool operator<(const Money& o)  const { return cents_ < o.cents_; }
};

CRTP donne du polymorphisme statique : tu écris du code générique réutilisable sans coût de virtual call. Utilisé pour les "mixins" : ajouter du comportement à plusieurs classes via héritage de template.

Critique CRTP. En C++20+, operator<=> rend la moitié des usages de CRTP obsolètes. Concepts + déduction réduisent encore davantage le besoin. Reste utile pour expression templates, observer pattern statique, etc.

7.1 — std::thread avancé

#include <thread>
#include <iostream>

void tache(int id) {
    std::cout << "thread " << id << "\n";
}

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

    t1.join();   // attend la fin
    t2.join();

    // std::jthread (C++20) : se join automatiquement à la destruction
    //                       supporte std::stop_token pour annulation propre
    std::jthread t3{[](std::stop_token st) {
        while (!st.stop_requested()) { ... }
    }};
}
std::thread = pas exception-safe. Si un thread n'est pas joiné ou detaché avant sa destruction, std::terminate. Préfère std::jthread (C++20).

7.2 — Mutex et locks

#include <mutex>

std::mutex m;
int compteur = 0;

void incrementer() {
    std::lock_guard<std::mutex> lock(m);   // RAII : libère à la sortie
    ++compteur;
}

// std::unique_lock : plus flexible (peut être déverrouillé/relocké)
// std::scoped_lock (C++17) : verrouille plusieurs mutex sans deadlock
std::mutex m1, m2;
{
    std::scoped_lock lock{m1, m2};
    // section critique
}

// std::shared_mutex : reader/writer lock
#include <shared_mutex>
std::shared_mutex sm;
void lecture()   { std::shared_lock lock{sm}; /* plusieurs lecteurs */ }
void ecriture()  { std::unique_lock lock{sm}; /* écrivain unique */ }

Condition variables

#include <condition_variable>

std::mutex m;
std::condition_variable cv;
std::queue<int> q;
bool done = false;

// Producteur
{
    std::lock_guard lock{m};
    q.push(42);
}
cv.notify_one();

// Consommateur
std::unique_lock lock{m};
cv.wait(lock, []{ return !q.empty() || done; });
if (!q.empty()) {
    int v = q.front(); q.pop();
}
Pièges concurrence.
  • Race conditions : accès non synchronisé → UB. Le sanitizer ThreadSanitizer (-fsanitize=thread) les détecte.
  • Deadlocks : N threads qui s'attendent en cercle. Évite en verrouillant les mutex toujours dans le même ordre, ou utilise std::scoped_lock.
  • Spurious wakeups : cv.wait peut se réveiller sans notification. Toujours vérifier la condition dans une boucle (ou via le predicate de wait).
  • False sharing : deux variables différentes sur la même ligne de cache → contention invisible. Aligne avec alignas(std::hardware_destructive_interference_size).

7.3 — async, future, promise

#include <future>

int calcul(int x) { /* ... */ return x * 2; }

auto f = std::async(std::launch::async, calcul, 42);
int r = f.get();   // bloque jusqu'au résultat

// Promise/future manuel
std::promise<int> p;
std::future<int> fut = p.get_future();
std::thread t{[&p]{ p.set_value(42); }};
std::cout << fut.get();
t.join();
Piège std::async. Sans std::launch::async explicite, l'implémentation peut choisir deferred (exécution paresseuse à get, pas en parallèle). Toujours préciser le mode si tu veux du parallélisme.

7.4 — Atomics et memory model expert

#include <atomic>

std::atomic<int> counter{0};
counter.fetch_add(1);     // thread-safe
counter++;                // idem

// Memory orders (du plus relax au plus strict) :
//   memory_order_relaxed   - juste l'atomicité
//   memory_order_consume   - rare et compliqué
//   memory_order_acquire   - lecture qui synchronise
//   memory_order_release   - écriture qui synchronise
//   memory_order_acq_rel
//   memory_order_seq_cst   - DÉFAUT, le plus simple/coûteux
counter.store(42, std::memory_order_release);
int v = counter.load(std::memory_order_acquire);

// Compare-and-exchange
int expected = 10;
bool ok = counter.compare_exchange_strong(expected, 20);
Lock-free programming = expert level. N'écris des structures lock-free que si tu sais ce que tu fais. La plupart des problèmes de concurrence se résolvent élégamment avec mutex + condition_variable. Atomics avec memory_order non-default = territoire de Herb Sutter, Anthony Williams. Lis "C++ Concurrency in Action" avant.

8.1 — Concepts (C++20) expert

#include <concepts>

// Concept = contrainte sur un type
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

template <Numeric T>          // syntaxe contrainte
T moyenne(T a, T b) { return (a + b) / 2; }

// Concept maison
template <typename T>
concept Printable = requires(T x) {
    { std::cout << x } -> std::same_as<std::ostream&>;
};

void log(Printable auto x) { std::cout << x; }
Concepts > SFINAE. Les messages d'erreur sont radicalement plus clairs. concept remplace enable_if dans 99% des cas. Utilise les concepts standard quand tu peux : std::integral, std::regular, std::invocable, std::ranges::range, etc.

8.2 — Ranges (C++20)

#include <ranges>
#include <algorithm>

std::vector<int> v{1, 2, 3, 4, 5, 6, 7, 8};

// Ranges algorithms : prennent des conteneurs entiers
std::ranges::sort(v);
auto n = std::ranges::count(v, 3);

// Views : lazy, composables, zero-cost
auto resultat = v
    | std::views::filter([](int x) { return x % 2 == 0; })
    | std::views::transform([](int x) { return x * x; })
    | std::views::take(3);

for (int r : resultat) std::cout << r << ' ';  // 4 16 36

Views ne copient rien. Elles décrivent une transformation paresseuse. Les éléments sont calculés à la demande lors de l'itération.

8.3 — Coroutines (C++20) expert

Une coroutine est une fonction qui peut suspendre son exécution et la reprendre plus tard. Trois mots-clés : co_await, co_yield, co_return.

#include <coroutine>
#include <generator>  // C++23

std::generator<int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        int next = a + b;
        a = b;
        b = next;
    }
}

for (int v : fibonacci() | std::views::take(10)) {
    std::cout << v << ' ';
}
Coroutines C++ : feature ultra puissante mais infrastructure manquante. Le standard fournit le mécanisme bas niveau ; il manque les types haut niveau (task, generator, channels…) en C++20. std::generator arrive en C++23. Pour le reste, des bibliothèques tierces : cppcoro, libunifex, asio.

8.4 — Modules (C++20)

// math.cppm — interface de module
export module math;

export int somme(int a, int b) { return a + b; }
export int produit(int a, int b) { return a * b; }

// main.cpp
import math;
int main() { return somme(2, 3); }

Les modules remplacent #include : isolation, compilation rapide, pas de pollution macro. Support compilateur encore inégal en 2026 — viser GCC 14+, Clang 17+, MSVC 2022 récent. Le tooling (build systems) rattrape progressivement.

9.1 — Design patterns essentiels en C++

RAII (déjà vu)

Le pattern le plus important du C++.

PIMPL (Pointer to Implementation)

// foo.h — l'utilisateur n'a besoin que de ça
class Foo {
public:
    Foo();
    ~Foo();
    void bar();
private:
    struct Impl;
    std::unique_ptr<Impl> impl_;
};

// foo.cpp
struct Foo::Impl { /* tous les détails ici */ };
Foo::Foo() : impl_{std::make_unique<Impl>()} {}
Foo::~Foo() = default;   // DOIT être dans le .cpp où Impl est complet
void Foo::bar() { impl_->bar(); }

PIMPL réduit les dépendances, accélère la compilation, garantit la stabilité ABI. Coût : indirection + allocation.

Factory

class Forme { public: virtual ~Forme() = default; };
class Carre : public Forme { ... };
class Cercle : public Forme { ... };

std::unique_ptr<Forme> creer_forme(const std::string& type) {
    if (type == "carre") return std::make_unique<Carre>();
    if (type == "cercle") return std::make_unique<Cercle>();
    throw std::invalid_argument("type inconnu");
}

Observer / Signals

L'objet observable garde une liste de callbacks. Quand son état change, il les appelle. En C++ : std::vector<std::function<void(...)>>, ou bibliothèques comme Boost.Signals2.

Singleton (Meyers)

class Logger {
public:
    static Logger& instance() {
        static Logger inst;     // thread-safe init garanti depuis C++11
        return inst;
    }
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
private:
    Logger() = default;
};
Singleton : à éviter sauf cas très précis. Couplage global, tests difficiles, ordre d'initialisation/destruction parfois indéfini. Préfère l'injection de dépendance.

Strategy / Type erasure

// std::function — type erasure pour callables
std::function<int(int)> f = [](int x) { return x * 2; };

// std::any (C++17) — peut contenir n'importe quel type
std::any a = 42;
a = std::string{"hello"};
auto s = std::any_cast<std::string>(a);

// std::variant (C++17) — union typée et sûre
std::variant<int, double, std::string> v = 42;
v = "hello";
std::visit([](auto&& x){ std::cout << x; }, v);

9.2 — Optimisation et bonnes pratiques

  1. Mesure d'abord. Les intuitions sur la perf sont souvent fausses. perf, callgrind, vtune, microbenchmarks (nanobench, Google Benchmark).
  2. Cache locality > tout. Un std::vector bat presque toujours std::list et arbres, même pour des opérations "asymptotiquement plus chères". La RAM est la nouvelle disquette.
  3. Évite les allocations dans la boucle chaude. reserve(), object pools, arena allocators.
  4. Préfère emplace_back à push_back pour des objets non triviaux. Évite les copies/moves intermédiaires.
  5. Préfère le passage par valeur + move dans les setters quand la fonction stocke l'argument :
    void set_nom(std::string n) { nom_ = std::move(n); }
    // Appelant avec lvalue → 1 copie. Avec rvalue → 0 copie. Optimal.
  6. RVO/NRVO — Return Value Optimization. Le compilateur élide la copie de retour. Encouragée :
    Widget make() {
        Widget w;
        w.configure();
        return w;   // NRVO : pas de copie
    }
  7. Move semantics — déplace les gros objets, ne les copie pas. std::vector, std::string, etc., supportent le move depuis C++11.
  8. Inline et constexpr pour les petites fonctions calculables à la compilation.
  9. Évite les virtual dans les hot paths. Vptr = indirection. Le compilateur ne peut pas inliner. Si le polymorphisme n'est pas vraiment nécessaire : CRTP, std::variant, ou monomorphisation par templates.
  10. Profile-Guided Optimization (PGO) et Link-Time Optimization (LTO) : drapeaux -flto, -fprofile-generate/-fprofile-use.

9.3 — Pièges classiques (à savoir détecter)

PiègeDescriptionSolution
Most Vexing ParseWidget w(); déclare une fonction !Widget w{};
SlicingPolymorphisme par valeur perd le type dynamiqueRéférence/pointeur
Iterator invalidationModifier un container invalide ses itérateursRé-acquérir, indices, copies
Self-assignmentx = x dans operator= mal écritif (this != &other) ou copy-and-swap
Double-freePointeur libéré deux foisSmart pointers
Use-after-moveUtiliser un objet déplacéDiscipline + sanitizers
Dangling reference/viewRef ou string_view sur objet détruitVérifier les durées de vie
Integer overflow signéUB en C++Types non-signés là où c'est sémantique, ou checks
Sequence point UBi = i++ + ++iUne seule modification par expression
Static init order fiascoGlobals dans des TUs différentesSingleton Meyers, init explicite
Active les sanitizers en dev.
g++ -std=c++20 -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer prog.cpp
AddressSanitizer + UBSanitizer attrapent 95% des bugs mémoire/UB. ThreadSanitizer (-fsanitize=thread) pour les races. Coût d'exécution acceptable en debug. Rien de comparable n'existe pour découvrir des bugs.

Liste complète des exercices

Chaque exercice contient deux fichiers :

  • exercice.cpp — le squelette à compléter (avec TODOs)
  • solution.cpp — la correction commentée

Compile chaque exercice avec :

g++ -std=c++20 -Wall -Wextra -O2 exercice.cpp -o prog && ./prog

10.1 — Pourquoi tester ? avancé

Sans tests automatisés, tu codes en aveugle. Tu vérifies à la main, tu oublies des cas, tu casses du code existant en ajoutant des features. Une fois en équipe : tu casses le code des autres, et eux le tien.

Ce que les tests t'apportent en pratique :

  • Régression : si je change ce code, est-ce que j'ai cassé quelque chose ailleurs ? Le CI lance tous les tests à chaque commit.
  • Documentation vivante : un test bien écrit montre comment utiliser une API mieux qu'un README.
  • Design : une classe difficile à tester est généralement mal conçue (couplage trop fort, dépendances cachées). Le test t'oblige à découper.
  • Refactor sans peur : tu peux restructurer si les tests passent encore.
Critique pour entreprise. En IMDS / vision par ordinateur, les tests sur du code numérique sont délicats : précision flottante, dépendance à des modèles ML, images d'entrée volumineuses. Pratiques courantes :
  • Tests unitaires sur la logique pure (parsing, calcul géométrique, normalisation de strings).
  • Tests d'intégration sur des fixtures images (jeu de test versionné) — on vérifie une métrique (IoU, taux d'erreur OCR) et un seuil.
  • Tests de bout en bout sur un dataset gold avec rapport HTML.
  • Tests de performance : un benchmark qui échoue si on perd 10% de FPS.

10.2 — Google Test (gtest) — le standard

Le framework de test le plus utilisé en C++ industriel. Bibliothèque header-only après installation. Intégration native avec CMake.

// calculatrice_test.cpp
#include <gtest/gtest.h>
#include "calculatrice.h"

TEST(Calculatrice, AdditionSimple) {
    EXPECT_EQ(somme(2, 3), 5);
    EXPECT_EQ(somme(-1, 1), 0);
}

TEST(Calculatrice, DivisionParZero) {
    EXPECT_THROW(divise(10, 0), std::domain_error);
}

TEST(Calculatrice, FlotantsApproches) {
    EXPECT_NEAR(divise(1.0, 3.0), 0.333, 0.001);
}

// Fixture : setup partagé
class InventaireTest : public ::testing::Test {
protected:
    void SetUp() override {
        inv.ajouter({"pomme", 10, 0.5});
        inv.ajouter({"orange", 5, 0.8});
    }
    Inventaire inv;
};

TEST_F(InventaireTest, ValeurTotale) {
    EXPECT_DOUBLE_EQ(inv.valeur_totale(), 9.0);
}

TEST_F(InventaireTest, RetirerExistant) {
    EXPECT_TRUE(inv.retirer("pomme"));
    EXPECT_EQ(inv.taille(), 1u);
}

// Tests paramétrés
class PalindromeTest : public ::testing::TestWithParam<std::string> {};

TEST_P(PalindromeTest, EstReconnu) {
    EXPECT_TRUE(est_palindrome(GetParam()));
}

INSTANTIATE_TEST_SUITE_P(Cas, PalindromeTest,
    ::testing::Values("kayak", "radar", "a", ""));

Macros essentielles

MacroEffet
EXPECT_EQ(a, b)Vérifie a==b ; continue si échec
ASSERT_EQ(a, b)Vérifie a==b ; arrête le test si échec
EXPECT_NEAR(a, b, eps)|a-b| < eps (flottants)
EXPECT_THROW(expr, E)expr doit lancer E
EXPECT_NO_THROW(expr)expr ne doit rien lancer
EXPECT_TRUE / FALSEBooléen
EXPECT_DEATH(expr, regex)expr doit faire crasher (assert)
EXPECT_ vs ASSERT_ : EXPECT continue après échec (tu vois plusieurs erreurs en un run) ; ASSERT arrête (utile quand la suite n'a plus de sens, ex: pointeur null que tu allais déréférencer).

10.3 — Catch2 — l'alternative ergonomique

#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>

TEST_CASE("Somme entiers", "[calc]") {
    REQUIRE(somme(2, 3) == 5);

    SECTION("avec négatifs") {
        REQUIRE(somme(-2, -3) == -5);
    }
    SECTION("avec zero") {
        REQUIRE(somme(0, 5) == 5);
    }
}

TEST_CASE("Division par zéro") {
    REQUIRE_THROWS_AS(divise(10, 0), std::domain_error);
}

TEST_CASE("Float matcher") {
    using Catch::Matchers::WithinAbs;
    REQUIRE_THAT(divise(1.0, 3.0), WithinAbs(0.333, 0.001));
}

Catch2 est plus léger à intégrer (single header v2, package CMake en v3). Syntaxe BDD-style possible (GIVEN/WHEN/THEN). Les SECTIONs sont rejouées indépendamment — chaque section a son propre setup.

Tu choisis…Si
Google TestProjet sérieux, integration mocking (gMock), grosse boîte, large écosystème
Catch2Setup rapide, code source moderne, syntaxe expressive, projets moyens
doctestTests dans le même fichier que le code (rare en prod, super en proto)

10.4 — Mocks (gMock)

Un mock est un faux objet qui simule une dépendance pour tester en isolation. Indispensable quand le composant testé dépend de réseau, fichiers, BDD, hardware.

// Interface à mocker
class IFrameSource {
public:
    virtual ~IFrameSource() = default;
    virtual cv::Mat capture() = 0;
    virtual bool est_ouverte() const = 0;
};

// Mock
class MockFrameSource : public IFrameSource {
public:
    MOCK_METHOD(cv::Mat, capture, (), (override));
    MOCK_METHOD(bool, est_ouverte, (), (const, override));
};

TEST(Pipeline, ArreteSiSourceFermee) {
    MockFrameSource src;
    EXPECT_CALL(src, est_ouverte()).WillOnce(::testing::Return(false));

    Pipeline p{&src};
    EXPECT_FALSE(p.tourner());
}

TEST(Pipeline, TraiteImageRecue) {
    MockFrameSource src;
    cv::Mat fake = cv::Mat::zeros(480, 640, CV_8UC3);

    EXPECT_CALL(src, est_ouverte()).WillRepeatedly(::testing::Return(true));
    EXPECT_CALL(src, capture()).WillOnce(::testing::Return(fake));

    Pipeline p{&src};
    auto res = p.traiter_une_frame();
    EXPECT_FALSE(res.empty());
}
Mocking et design. Pour mocker, il te faut une interface (classe abstraite ou template). Si ton code instancie directement cv::VideoCapture en dur dans la classe, tu ne peux pas le tester sans une vraie caméra. C'est l'injection de dépendance : le composant reçoit ses dépendances de l'extérieur. Indispensable pour la testabilité.

10.5 — TDD et pratique du test

Test-Driven Development : tu écris le test avant le code. Cycle Red-Green-Refactor :

  1. Red — écris un test qui échoue (la feature n'existe pas encore).
  2. Green — écris le minimum de code pour le faire passer.
  3. Refactor — nettoie sans casser le test.

Tu n'es pas obligé de faire du TDD strict. En vrai code industriel, un mix marche bien :

  • TDD pour la logique métier pure et les algorithmes.
  • Tests après pour le code exploratoire (proto vision par ex.).
  • Tests de régression dès qu'un bug est trouvé : reproduire le bug en test, fixer.

Métriques de qualité

  • Couverture de code (gcov/lcov, ou llvm-cov). Vise 70-90% sur le métier ; ne fétichise pas le 100%.
  • Tests rapides : la suite doit tourner en quelques secondes. Sinon les devs ne la lancent plus.
  • Tests indépendants : pas d'ordre, pas de fichiers partagés non nettoyés.

Compilation type avec gtest

# Ubuntu
sudo apt install libgtest-dev libgmock-dev

g++ -std=c++20 -Wall test_calc.cpp calc.cpp \
    -lgtest -lgtest_main -lgmock -pthread \
    -o test_calc && ./test_calc

11.1 — Sanitizers : ton meilleur ami critique

Les sanitizers sont des outils intégrés à GCC/Clang qui instrumentent ton code pour détecter automatiquement des bugs mémoire, des comportements indéfinis, des races. Ils ne remplacent pas les tests : ils les amplifient.

SanitizerDrapeauDétecte
AddressSanitizer (ASan)-fsanitize=addressOut-of-bounds, use-after-free, double-free, fuites
UndefinedBehaviorSanitizer (UBSan)-fsanitize=undefinedOverflow signé, déréférencement nullptr, shifts invalides…
ThreadSanitizer (TSan)-fsanitize=threadRaces, deadlocks (incompatible avec ASan)
MemorySanitizer (MSan)-fsanitize=memory (Clang)Lecture de mémoire non initialisée
LeakSanitizer (LSan)inclus dans ASanFuites mémoire à la fin du programme
# Build de dev recommandé
g++ -std=c++20 -Wall -Wextra -O1 -g \
    -fsanitize=address,undefined \
    -fno-omit-frame-pointer \
    main.cpp -o prog

./prog   # tout bug est rapporté avec stack trace
Sanitizers en pratique.
  • Coût d'exécution : ASan ~2x, UBSan ~1.2x. Acceptable en dev/CI, pas en prod.
  • Exécute tes tests sous sanitizers en CI. Pas que ton main.
  • ASan et TSan sont incompatibles. Build deux fois.
  • Pour la perf, on retire les sanitizers du build release. Garde-les en debug et en CI.
Cas réel : ASan a sauvé Anthropic, Google, Meta et tout le monde. Si tu n'utilises pas ASan, tu as des bugs mémoire que tu ne vois pas — point.

Exemple de rapport ASan

==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x60200000eff0
READ of size 4 at 0x60200000eff0 thread T0
    #0 0x4012a3 in main /home/me/bug.cpp:8
freed by thread T0 here:
    #0 0x7f...___interceptor_free
    #1 0x401269 in main /home/me/bug.cpp:6
previously allocated by thread T0 here:
    ...

11.2 — Valgrind

Outil historique sur Linux. Plus lent qu'ASan (10-50x), mais ne nécessite pas de recompilation. Utile sur des binaires release ou des softs tiers.

# Détection de fuites — l'usage le plus courant
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./prog

# Détection de races (lent)
valgrind --tool=helgrind ./prog

# Profiling de cache (callgrind + KCachegrind)
valgrind --tool=callgrind ./prog
kcachegrind callgrind.out.12345
Valgrind vs ASan. Si tu peux recompiler, prends ASan : 5-10x plus rapide, plus précis, supporte Windows (avec Clang). Valgrind n'est utile que sur binaire opaque ou pour Massif (heap profiling) et Callgrind (call graph).

11.3 — GDB : le debugger

# Compile avec symboles
g++ -g -O0 -std=c++20 prog.cpp -o prog

gdb ./prog
CommandeAction
run [args] / rLance le programme
break file.cpp:42 / bBreakpoint à ligne
break MaClasse::methodeBreakpoint sur fonction
continue / cReprend l'exécution
next / nStep over (ligne suivante)
step / sStep into (entre dans la fonction)
finishSort de la fonction courante
print var / pAffiche une variable
watch varStop quand var change
backtrace / btStack trace
info localsVariables locales
frame N / f NAller à frame N
thread NBascule de thread

Debug post-mortem (core dump)

# Active les core dumps
ulimit -c unlimited

# Quand le programme crashe → core généré
gdb ./prog core
(gdb) bt              # pile au moment du crash
Pretty printers. GDB affiche par défaut std::vector, std::string, etc. comme des structures opaques. Sous GCC, les pretty-printers Python sont activés par défaut. Sinon, fichier ~/.gdbinit :
python
import sys
sys.path.insert(0, '/usr/share/gcc/python')
from libstdcxx.v6.printers import register_libstdcxx_printers
register_libstdcxx_printers(None)
end

11.4 — Trouver une fuite mémoire critique

Une fuite n'est pas un crash : ton programme tourne, mais consomme plus de RAM avec le temps. En IMDS, traiter 30 fps avec une fuite de 1 Mo par frame → 1.8 Go/min. Mort en quelques minutes.

Méthodes par ordre d'efficacité

  1. Évite les fuites par design : RAII, smart pointers, containers. La majorité des fuites en C++ moderne viennent de cas non-idiomatiques.
  2. ASan (LeakSanitizer intégré). Lance ton programme, quitte proprement, ASan rapporte tout ce qui n'a pas été libéré :
    =================================================================
    ==1234==ERROR: LeakSanitizer: detected memory leaks
    
    Direct leak of 40 byte(s) in 1 object(s) allocated from:
        #0 0x... in operator new(unsigned long)
        #1 0x... in fonction_qui_fuit src/foo.cpp:42
        #2 0x... in main main.cpp:15
    
  3. Valgrind sur le binaire :
    valgrind --leak-check=full --show-leak-kinds=all --error-exitcode=1 ./prog
    Catégories : definitely lost (vraies fuites), indirectly lost (cascade), possibly lost (pointeur intérieur), still reachable (singletons jamais libérés — pas grave en général).
  4. Heaptrack : profileur graphique des allocations.
    heaptrack ./prog
    heaptrack_gui heaptrack.prog.12345.gz
    Voit qui alloue le plus, où, et quand. Excellent pour de la perf mémoire (pas seulement fuites).
  5. Pression mémoire au runtime : log la RSS du processus toutes les N secondes. Si elle augmente linéairement → fuite. Sous Linux : lire /proc/self/status, champ VmRSS.

Cas typiques en C++ moderne (oui, ça arrive encore)

  • Cycle de shared_ptr — la cause #1 quand tu as des graphes/observers. Solution : weak_ptr sur un côté.
  • Capture this dans une lambda stockée, l'objet meurt avant la lambda → en plus c'est un use-after-free.
  • Container qui grossit indéfiniment : un map<Key, Value> où on n'erase jamais (cache sans LRU).
  • Bibliothèque C qu'on ne libère pas : cvCreate* (legacy OpenCV C-API), fopen sans fclose, FFmpeg avcodec_alloc sans free…

11.5 — Profiling perf

Tu n'as pas le droit d'optimiser sans mesurer. "Premature optimization is the root of all evil" — Knuth, mais aussi "oui mais après avoir mesuré".

Outils par cas d'usage

OutilPour
perf (Linux)Hot functions, top-down, cache misses, branch misprediction
callgrind (Valgrind)Call graph précis (instrumentation)
VTune (Intel)Le plus complet, GUI riche, gratuit
heaptrackAllocations
Google BenchmarkMicrobenchmarks reproductibles
TracyProfiler temps réel pour gameloop / pipeline
# perf — outils standards Linux
perf stat ./prog              # compteurs HW (cycles, cache, branches)
perf record -g ./prog         # enregistre profil (call stacks)
perf report                   # navigateur interactif

# Génère un flamegraph
perf record -F 99 -g ./prog
perf script | flamegraph.pl > out.svg

Microbenchmark avec Google Benchmark

#include <benchmark/benchmark.h>

static void BM_VectorPush(benchmark::State& state) {
    for (auto _ : state) {
        std::vector<int> v;
        for (int i = 0; i < state.range(0); ++i) {
            v.push_back(i);
        }
        benchmark::DoNotOptimize(v);
    }
}
BENCHMARK(BM_VectorPush)->Range(8, 8<<10);

BENCHMARK_MAIN();
Compiler Explorer (godbolt.org) : pour voir l'assembleur généré côte à côte avec ton C++. Indispensable pour comprendre ce que le compilo fait vraiment et pour valider qu'une « optimisation » a un effet.

12.1 — CMake : la base avancé

CMake est l'outil de build de fait en C++. Il génère des Makefiles, des projets Ninja, Visual Studio, Xcode. Les bibliothèques sérieuses fournissent un CMakeLists.txt. À ne pas confondre avec un build system : CMake est un générateur, le vrai build est fait par Make/Ninja/MSBuild.

Hello CMake

# CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(MonProjet VERSION 0.1 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)        # pas d'extensions GNU

add_executable(prog main.cpp utils.cpp)

target_compile_options(prog PRIVATE
    -Wall -Wextra -Wpedantic
    $<$<CONFIG:Debug>:-O0 -g -fsanitize=address,undefined>
    $<$<CONFIG:Release>:-O2>
)
target_link_options(prog PRIVATE
    $<$<CONFIG:Debug>:-fsanitize=address,undefined>
)

Cycle de build

mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Debug
cmake --build . -j

# Build release séparé
cmake -B build-rel -DCMAKE_BUILD_TYPE=Release
cmake --build build-rel -j

# Avec un autre générateur
cmake -B build -G Ninja
ninja -C build

12.2 — Projet structuré

monprojet/
├── CMakeLists.txt
├── include/
│   └── monprojet/
│       └── api.h           # headers publics
├── src/
│   ├── api.cpp
│   └── interne/
│       └── helper.cpp
├── tests/
│   ├── CMakeLists.txt
│   ├── api_test.cpp
│   └── helper_test.cpp
├── third_party/            # dépendances vendored (optionnel)
└── apps/
    └── cli.cpp             # exécutable final
# CMakeLists.txt racine
cmake_minimum_required(VERSION 3.20)
project(monprojet VERSION 0.1 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Bibliothèque statique réutilisable
add_library(monprojet_lib STATIC
    src/api.cpp
    src/interne/helper.cpp
)
target_include_directories(monprojet_lib
    PUBLIC  include
    PRIVATE src
)
target_compile_features(monprojet_lib PUBLIC cxx_std_20)

# Exécutable
add_executable(monprojet_cli apps/cli.cpp)
target_link_libraries(monprojet_cli PRIVATE monprojet_lib)

# Tests (optionnels)
option(MONPROJET_BUILD_TESTS "Build tests" ON)
if(MONPROJET_BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()
# tests/CMakeLists.txt
find_package(GTest REQUIRED)

add_executable(monprojet_tests
    api_test.cpp
    helper_test.cpp
)
target_link_libraries(monprojet_tests PRIVATE
    monprojet_lib
    GTest::gtest GTest::gtest_main
)

include(GoogleTest)
gtest_discover_tests(monprojet_tests)
Modern CMake = target-based. Oublie les include_directories globaux, add_compile_options, etc. Tout passe par target_* avec scope (PRIVATE/PUBLIC/INTERFACE). PUBLIC = utilisé par moi et propagé aux consommateurs ; PRIVATE = juste moi ; INTERFACE = juste les consommateurs (pour les libs header-only).

12.3 — Dépendances : OpenCV, Boost, etc.

find_package — bibliothèques système

find_package(OpenCV REQUIRED COMPONENTS core imgproc imgcodecs videoio)
find_package(Threads REQUIRED)
find_package(fmt REQUIRED)

target_link_libraries(monprojet_lib
    PUBLIC ${OpenCV_LIBS}
    PRIVATE Threads::Threads fmt::fmt
)

FetchContent — récupère du source

include(FetchContent)
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)

Conan / vcpkg — gestionnaires de paquets

vcpkg (Microsoft) et Conan sont les deux gestionnaires sérieux. Ils résolvent les dépendances, gèrent le cache binaire, supportent multi-plateforme. En entreprise, l'un ou l'autre est quasi obligatoire dès qu'il y a 5+ dépendances.

# vcpkg manifest mode — vcpkg.json à la racine
{
  "name": "monprojet",
  "version": "0.1.0",
  "dependencies": [
    "opencv4", "fmt", "spdlog", "gtest", "benchmark"
  ]
}

# Build avec vcpkg
cmake -B build -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake

13.1 — Git en équipe

Pas un cours de Git, mais les pratiques incontournables :

  • Branches courtes. feature/ocr-improvements, fix/leak-in-pipeline. Vit quelques jours, pas plusieurs mois.
  • Commits atomiques avec messages clairs. Une modif logique = un commit. Ne mélange pas refactor et fix.
  • Convention de message : par exemple Conventional Commits — feat(ocr): add confidence threshold, fix(alpr): handle empty plate. Permet le changelog auto.
  • Rebase pour mettre à jour ta branche, pas merge. Garde un historique linéaire.
  • Pull requests / Merge requests : passage obligé pour intégrer. CI doit être verte.
# Workflow type
git checkout -b feature/nouvelle-fonctionnalité
# ... bosse, commit ...
git rebase main                  # rebase régulièrement sur main
git push origin feature/nouvelle-fonctionnalité
# Ouvre une MR/PR ; review ; merge.

.gitignore C++ minimum

# Build
build/
build-*/
out/
*.o
*.obj
*.so
*.a
*.dylib
*.exe

# IDE
.vscode/
.idea/
*.swp

# CMake
CMakeCache.txt
CMakeFiles/
cmake_install.cmake

# Tests
*.gcda
*.gcno
coverage.info

13.2 — Code review : ce qu'on regarde

  • Correction : le code fait ce qu'il dit. Edge cases : null, vide, négatif, gigantesque, concurrent.
  • Sécurité mémoire : pas de new/delete brut, pas de raw pointer propriétaire, durées de vie claires.
  • Tests : la modif a des tests. Pas seulement le chemin heureux.
  • Performance évidente : pas de copie dans la boucle chaude, const& pour les paramètres lourds, reserve avant push en boucle.
  • Lisibilité : noms qui parlent, fonctions courtes, niveau d'imbrication faible. Si tu dois commenter "pourquoi" trois fois, c'est qu'il faut refactor.
  • Cohérence avec le reste du code : conventions de nommage, style.
  • Pas de dead code : code commenté, fonctions inutilisées, TODO sans ticket.
Outils statiques.
  • clang-format — formatage auto. Définis un .clang-format à la racine. Lance-le en pre-commit hook.
  • clang-tidy — linter avancé. Détecte thousands de bugs et anti-patterns. Configure avec .clang-tidy.
  • cppcheck — analyseur statique léger.
  • include-what-you-use (iwyu) — minimise les #include.
# .clang-tidy minimal
Checks: '*,
  -fuchsia-*,
  -google-readability-todo,
  -modernize-use-trailing-return-type,
  -llvm-header-guard'
WarningsAsErrors: ''

13.3 — CI/CD : automatisation

Chaque push doit déclencher un build + tests + lint. Sans CI, tu vis dangereusement.

# .github/workflows/ci.yml — exemple GitHub Actions
name: CI
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Install
      run: |
        sudo apt update
        sudo apt install -y g++-13 cmake ninja-build libgtest-dev libopencv-dev
    - name: Configure (Debug + ASan)
      run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
    - name: Build
      run: cmake --build build
    - name: Test
      run: ctest --test-dir build --output-on-failure
    - name: Lint
      run: clang-tidy src/*.cpp -- -Iinclude -std=c++20

En entreprise, c'est plus complexe : matrices Linux/Windows/macOS, builds release+debug, sanitizers, couverture, déploiement Docker, signature de binaires…

13.4 — Logging

std::cout est insuffisant pour le réel. Il te faut :

  • Niveaux : DEBUG, INFO, WARN, ERROR, CRITICAL
  • Timestamps
  • Thread ID (essentiel pour du multithread)
  • Sortie configurable : console, fichier rotatif, syslog
  • Performance : pas de log bloquant le hot path

La bibliothèque standard de fait : spdlog (header-only, fast, thread-safe). Alternative : Boost.Log, glog (Google).

#include <spdlog/spdlog.h>
#include <spdlog/sinks/rotating_file_sink.h>

int main() {
    auto file_logger = spdlog::rotating_logger_mt(
        "app", "logs/app.log", 5 * 1024 * 1024, 3);
    spdlog::set_default_logger(file_logger);
    spdlog::set_level(spdlog::level::debug);
    spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [%t] %v");

    SPDLOG_INFO("Démarrage");
    SPDLOG_WARN("Plaque {} confiance basse: {:.2f}", "AB-123-CD", 0.42);
    SPDLOG_ERROR("Échec init caméra: {}", code_erreur);
}
Niveaux : à respecter.
  • ERROR = quelque chose qu'il faut investiguer. Si t'en as 1000/sec, plus personne ne regarde.
  • WARN = anormal mais récupérable. Une plaque illisible n'est pas un WARN, c'est attendu.
  • INFO = événement métier important. Démarrage, arrêt, traitement d'un batch.
  • DEBUG = pour le dev. Désactivé en prod.

13.5 — Conventions et style

L'équipe choisit, tu suis. Les conventions populaires :

  • Google C++ Style Guide — strict, beaucoup adopté. kConstNames, VariableName, function_name.
  • LLVMVariableName, FunctionName.
  • Style « C++ moderne » — souvent : snake_case pour fonctions/variables, PascalCase pour types, UPPER_CASE pour macros, member_ ou m_member pour les membres.

Ce qui compte plus que le choix lui-même :

  • Cohérence à l'échelle du projet.
  • Auto-formatage via .clang-format versionné. Personne ne discute du formatage en review.
  • Header guards uniformes : #pragma once est OK partout en pratique, ou guards PROJET_FICHIER_H_.

14.1 — OpenCV : essentiels pour IMDS expert

OpenCV est la bibliothèque de vision par ordinateur. C++ natif, GPU (CUDA/OpenCL), bindings Python. En IMDS Software (analyse d'image, OCR, plaques, visages), tu vas vivre dedans.

Type central : cv::Mat

#include <opencv2/opencv.hpp>

int main() {
    cv::Mat img = cv::imread("plaque.jpg", cv::IMREAD_COLOR);
    if (img.empty()) {
        std::cerr << "image non chargée\n";
        return 1;
    }
    std::cout << "taille: " << img.cols << "x" << img.rows
              << " canaux: " << img.channels()
              << " type: " << img.type() << '\n';

    cv::Mat gris;
    cv::cvtColor(img, gris, cv::COLOR_BGR2GRAY);

    cv::Mat flou;
    cv::GaussianBlur(gris, flou, {5, 5}, 0);

    cv::Mat contours;
    cv::Canny(flou, contours, 50, 150);

    cv::imshow("contours", contours);
    cv::waitKey(0);
    return 0;
}
cv::Mat : sémantique de copie surprenante.
cv::Mat a = cv::imread("x.jpg");
cv::Mat b = a;             // PARTAGE le buffer (compteur de réf interne)
b.at<cv::Vec3b>(0,0) = ...; // modifie AUSSI a

cv::Mat c = a.clone();     // VRAIE copie profonde
C'est volontaire (perf) mais piégeux. Quand tu écris dans une cv::Mat que tu as reçue, tu peux modifier la matrice de l'appelant. clone() ou copyTo() pour copier vraiment.

Types fréquents

CV_TYPESens
CV_8UC11 canal, uint8 — image grayscale
CV_8UC33 canaux uint8 — image BGR (pas RGB !)
CV_8UC4BGRA
CV_32FC1Float — pour calculs intermédiaires
CV_32FC3Float 3 canaux
OpenCV est en BGR, pas RGB. Historique : DirectShow Windows, machins legacy. Tu charges en BGR, tu sauvegardes en BGR. Pour interop avec autre chose (ML, autres libs), cv::cvtColor(img, img, COLOR_BGR2RGB).

14.2 — Pipeline d'images temps réel

L'architecture typique en IMDS / video analytics :

    Caméra/Fichier → Décodage → Pré-process → Détection → Reco → Fusion → Sortie
       (thread A)    (thread B)   (thread C)   (thread D)  (thread E) (thread F)

Chaque étage est un thread, communique via une queue thread-safe (cf. exercice 30). Le système doit :

  • Gérer le back-pressure : si la détection est trop lente, on droppe les frames trop anciennes.
  • Pas bloquer sur la sortie : log/db doivent être asynchrones.
  • Reprise automatique en cas d'erreur d'une caméra.
struct Frame {
    cv::Mat img;
    int64_t ts_ns;
    std::string camera_id;
};

ConcurrentQueue<Frame> capture_queue;
ConcurrentQueue<Frame> detect_queue;

// Thread capture
void capture_loop(cv::VideoCapture& cam, std::stop_token st) {
    Frame f;
    while (!st.stop_requested()) {
        if (!cam.read(f.img) || f.img.empty()) {
            spdlog::warn("frame manquée");
            continue;
        }
        f.ts_ns = std::chrono::steady_clock::now().time_since_epoch().count();
        capture_queue.push(std::move(f));
    }
}

// Thread détection
void detect_loop(Detector& det, std::stop_token st) {
    while (!st.stop_requested()) {
        auto f = capture_queue.pop();
        if (!f) break;

        // Drop si trop ancien (back-pressure)
        if (en_retard(f->ts_ns, 200'000'000)) {
            continue;
        }
        auto detections = det.run(f->img);
        detect_queue.push({std::move(*f), std::move(detections)});
    }
}

14.3 — OCR : reconnaissance de texte

Deux familles d'OCR :

  1. Classique : binarisation, segmentation, classification caractère. Tesseract est le leader open source. Bon pour du texte bien posé.
  2. Deep learning : end-to-end (PaddleOCR, EasyOCR, TrOCR) ou CRNN. Plus robuste pour scènes naturelles. Coût d'inférence plus élevé.

Tesseract via C++

#include <tesseract/baseapi.h>
#include <leptonica/allheaders.h>
#include <opencv2/opencv.hpp>

std::string extraire_texte(const cv::Mat& img) {
    tesseract::TessBaseAPI api;
    if (api.Init(nullptr, "fra", tesseract::OEM_LSTM_ONLY) != 0) {
        throw std::runtime_error("init Tesseract");
    }
    api.SetPageSegMode(tesseract::PSM_SINGLE_LINE);    // pour une plaque

    api.SetImage(img.data, img.cols, img.rows,
                 img.channels(), static_cast<int>(img.step));

    std::unique_ptr<char[]> texte{api.GetUTF8Text()};
    return texte ? std::string{texte.get()} : "";
}

Pré-traitement crucial

Sans pré-traitement, l'OCR donne du bruit. Pipeline typique :

  1. Conversion grayscale.
  2. Augmentation de contraste : CLAHE (Contrast Limited Adaptive Histogram Equalization).
  3. Débruitage : cv::bilateralFilter (préserve les contours), pas de Gaussian blur agressif.
  4. Binarisation : cv::adaptiveThreshold ou Otsu (cv::THRESH_OTSU).
  5. Désinclinaison (deskew) : projection / Hough.
  6. Upscaling : Tesseract aime ~30-60 px de hauteur de caractère. Sous-échantillonné = rate.
OCR en prod : la qualité du pré-traitement compte plus que le moteur. Une équipe IMDS passera plus de temps à ajuster le pipeline image qu'à brancher Tesseract. Le moteur doit voir une image propre, contrastée, à la bonne taille, droite.

14.4 — ALPR : Automatic License Plate Recognition

L'ALPR est un cas particulier d'OCR avec contraintes fortes : alphabet restreint (lettres + chiffres), format pays-dépendant, taille variable, lumière souvent terrible (rétro-éclairage, infrarouge la nuit).

Architecture typique

  1. Détection plaque dans la frame complète.
    • Classique : Haar cascades, HOG+SVM (rapide, sensible).
    • Moderne : YOLO/SSD entraîné sur des plaques (robuste, GPU dispo).
  2. Crop + redressement de la plaque (homographie depuis 4 coins détectés).
  3. Segmentation des caractères ou reconnaissance directe (CRNN).
  4. Validation format avec regex pays. Ex France : ^[A-Z]{2}-?\d{3}-?[A-Z]{2}$ (SIV) ou ancien FNI.
  5. Confiance et reject : si la confiance < seuil, on droppe au lieu de remonter une fausse plaque.

Bibliothèques C++ existantes

  • OpenALPR (legacy) — open source, basée Tesseract + détecteur. Datée mais marche.
  • Cascades OpenCV ou réseaux ONNX exécutés via cv::dnn.
  • Solutions commerciales : Anyline, PlateSmart, etc. — quand IMDS choisit son moteur.
// Inférence ONNX d'un détecteur via OpenCV DNN
cv::dnn::Net net = cv::dnn::readNetFromONNX("plate_detector.onnx");
net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);

cv::Mat blob = cv::dnn::blobFromImage(img, 1.0/255, {640, 640}, {}, true);
net.setInput(blob);
std::vector<cv::Mat> outputs;
net.forward(outputs, net.getUnconnectedOutLayersNames());
// → parser les outputs YOLO pour récupérer les bbox

Cas piège (entreprise)

  • Plaques sales / partiellement masquées — il faut un seuil de confiance et une logique de tracking inter-frames pour valider.
  • Plaques étrangères — multi-pays = multi-formats = multi-charsets. Conception extensible.
  • Réflexion infrarouge — les caméras ANPR utilisent souvent de l'IR. La plaque ressort très contrastée. Pré-traitement adapté.
  • Performance : sur un flux 25-30 fps multi-cam, le détecteur tourne à 10-30 ms/frame. Sans GPU, dur à tenir.

14.5 — Reconnaissance faciale

Trois étapes distinctes (à ne pas confondre) :

  1. Détection : où sont les visages ? (Haar, MTCNN, RetinaFace, YOLO-face)
  2. Alignement : 5 ou 68 landmarks → transformation affine pour normaliser l'orientation.
  3. Identification :
    • Vérification 1:1 — c'est bien Alice ?
    • Identification 1:N — qui est cette personne dans une base ?
    Implémenté via un réseau qui produit un embedding (vecteur 128-512 D). Comparaison par distance cosine ou L2.
// Pipeline simplifié avec OpenCV DNN
cv::dnn::Net detector = cv::dnn::readNetFromCaffe(
    "deploy.prototxt", "res10_300x300_ssd.caffemodel");
cv::dnn::Net embedder = cv::dnn::readNetFromTorch("openface.nn4.t7");

std::vector<cv::Rect> detecter_visages(const cv::Mat& img) {
    cv::Mat blob = cv::dnn::blobFromImage(
        img, 1.0, {300, 300}, {104, 177, 123});
    detector.setInput(blob);
    cv::Mat det = detector.forward();
    // parser et seuiller les détections...
    return {};
}

cv::Mat embedding(const cv::Mat& visage_aligne) {
    cv::Mat blob = cv::dnn::blobFromImage(
        visage_aligne, 1.0/255, {96, 96}, {}, true);
    embedder.setInput(blob);
    return embedder.forward().clone();   // 128-D float
}

double distance(const cv::Mat& a, const cv::Mat& b) {
    return cv::norm(a, b, cv::NORM_L2);
}

Bibliothèques sérieuses

  • dlib — historiquement la référence, ResNet 128-D, C++ propre.
  • InsightFace (ArcFace) — meilleurs embeddings actuels, ONNX disponible.
  • OpenCV face module — basique, pour démarrer.
  • SDKs commerciaux quand IMDS a un client final.
Légal et éthique. La reconnaissance faciale est fortement régulée en UE (RGPD, AI Act 2024). Tu ne fais pas n'importe quoi avec ces données : finalité légitime, base légale, durée de conservation, consentement quand il s'applique. Le code te concerne directement : anonymisation des logs, chiffrement des embeddings stockés, audit trail. Discute avec ton DPO avant de stocker quoi que ce soit.

14.6 — Performance pour traitement image temps réel

30 fps = 33 ms par frame total. Sur un flux 4K, c'est tendu. Techniques essentielles :

  1. Évite les allocations dans la boucle. Pré-alloue tes cv::Mat de travail, réutilise-les. cv::Mat::create ne réalloue que si la taille change.
  2. Réduis la résolution avant détection. Détecter sur 640×480 puis projeter sur 4K est 30x plus rapide.
  3. Threading par étage (cf. plus haut). Mais attention au coût des copies entre threads.
  4. SIMD : OpenCV exploite SSE/AVX/NEON automatiquement pour ses fonctions. Pour ton code custom, intrinsics ou autovectorisation (-O3, alignement, std::span).
  5. GPU via OpenCV CUDA, ONNX Runtime, TensorRT. Inférence ML ≥10x plus rapide qu'un CPU.
  6. Fixed-point au lieu de float quand applicable.
  7. Batching des frames si latence le permet (souvent non en temps réel).

Mesurer (toujours)

using Clock = std::chrono::steady_clock;

class Profiler {
    Clock::time_point start_;
    std::string label_;
public:
    explicit Profiler(std::string l) : start_{Clock::now()}, label_{std::move(l)} {}
    ~Profiler() {
        auto us = std::chrono::duration_cast<std::chrono::microseconds>(
            Clock::now() - start_).count();
        SPDLOG_DEBUG("[{}] {} us", label_, us);
    }
};

// Usage
void traiter(const cv::Mat& f) {
    Profiler p{"traitement"};
    // ... travail ...
}
FPS effective vs FPS de la caméra. Une cam 30 fps donne 30 frames/s, mais si ton pipeline en traite 12, tu en perds. Mesure les deux séparément, pose des compteurs dropped/processed/queued.

14.7 — Déploiement IMDS

Quelques choses spécifiques au déploiement de software de vision :

  • Cibles : serveur Linux x86_64 (datacenter), edge ARM (Jetson, Raspberry, NVR), Windows (postes), parfois embarqué.
  • Cross-compilation : ARM/edge depuis x86. CMake + toolchain file, ou Docker buildx.
  • Distribution Docker : image avec OpenCV + tes binaires + modèles. Multi-stage build pour ne pas embarquer le compilo.
  • Modèles ML versionnés à part du code (Git LFS, S3, ou registry). Charger par hash/version pour traçabilité.
  • Configuration externe (YAML/TOML) — pas de constante dans le code pour les seuils, paths, IPs caméras.
  • Métriques exposées (Prometheus) : FPS, latence pipeline, taux de détection, erreurs caméra.
  • Health checks : endpoint HTTP qui dit "ça tourne", "ça décode", "tous les modèles chargés".
  • Update : déploiements blue-green ou rolling, fallback à la version précédente.

Dockerfile type

# Build stage
FROM ubuntu:22.04 AS build
RUN apt-get update && apt-get install -y \
    g++ cmake ninja-build libopencv-dev libtesseract-dev
COPY . /src
WORKDIR /src
RUN cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
    && cmake --build build --target install

# Runtime stage : minimal
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
    libopencv-core4.5d libopencv-imgproc4.5d libtesseract4 \
    && rm -rf /var/lib/apt/lists/*
COPY --from=build /usr/local/bin/imds_pipeline /usr/local/bin/
COPY models/ /models/
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
    CMD curl -f http://localhost:8080/health || exit 1
ENTRYPOINT ["imds_pipeline", "--config", "/etc/imds/config.yaml"]

Énoncés détaillés des exercices

Pour chaque exercice : objectif, contraintes et indices critiques. Tu retrouves le squelette à compléter dans exercices/<section>/<exo>/exercice.cpp.

1. Bases débutant

01 — Hello, World !

Affiche "Bonjour, C++ !" puis ton prénom. Compare \n vs std::endl (flush).

📁 exercices/01-bases/01-hello-world/

02 — Variables et types

Lis prénom + année naissance + taille (mètres) au clavier. Affiche un récapitulatif. Pièges : std::cin >> vs std::getline, le \n résiduel.

📁 exercices/01-bases/02-variables/

03 — FizzBuzz

1..100. Multiples de 3 → "Fizz", 5 → "Buzz", 15 → "FizzBuzz". Bonus : extensible à de nouvelles règles sans réécrire.

📁 exercices/01-bases/03-conditions/

04 — Statistiques de saisie

Lis des réels jusqu'à -1. Affiche nombre, somme, moyenne (2 décimales), min, max. Piège : initialisation de min/max (à infinity, pas à 0).

📁 exercices/01-bases/04-boucles/

05 — Factorielle & Fibonacci

Implémente factorielle itérative, fibonacci récursif et itératif. Critique : type retour (long long), explosion exponentielle de fib récursif. Bonus : version mémoïsée.

📁 exercices/01-bases/05-fonctions/

06 — Analyse de notes

Sur un std::array<double, 10> : moyenne, min, max, nb >= seuil, affichage inversé. Piège majeur : size_t qui décroît jusqu'à -1 → underflow → boucle infinie.

📁 exercices/01-bases/06-tableaux/

2. Mémoire intermédiaire

07 — Pointeurs : swap, find, print

3 fonctions C-style (int* + size_t). Vérification nullptr. Critique : std::span serait plus moderne.

📁 exercices/02-memoire/01-pointeurs/

08 — Références

swap_ref, maximum_ref qui retourne une référence modifiable, doubler_tous sur un vector. Piège : ne JAMAIS retourner une référence vers une variable locale.

📁 exercices/02-memoire/02-references/

09 — Allocation dynamique

Manipulation directe de new[]/delete[] pour comprendre. Compile avec -fsanitize=address pour détecter les fuites. Conclusion : préférer std::vector.

📁 exercices/02-memoire/03-allocation/

10 — std::string

Palindrome (avec lower + skip espaces), reverse, split, join. Bonus : version std::string_view sans allocation.

📁 exercices/02-memoire/04-string/

3. POO intermédiaire

11 — Rectangle

Classe complète : ctor avec validation, aire, périmètre, setters validés (lance std::invalid_argument), méthode statique carre(cote).

📁 exercices/03-poo/01-rectangle/

12 — Vector3 mathématique

Surcharge complète : + - += -= * (scalaire) ==, méthodes norme dot cross normalise, operator<<. Marque constexpr.

📁 exercices/03-poo/02-vector3/

13 — Compte bancaire

Encapsulation avec invariants métier. Solde en centimes (long long), pas en double. Méthodes deposer, retirer, virer_vers atomique. operator<< formaté en €.

📁 exercices/03-poo/03-compte-bancaire/

14 — Hiérarchie de formes

Forme abstraite, dérivées Cercle/Rectangle/Triangle. Triangle : valider l'inégalité triangulaire ; aire par Héron. vector<unique_ptr<Forme>>. Crucial : destructeur virtuel.

📁 exercices/03-poo/04-heritage/

15 — Zoo polymorphe

Animal abstrait, Chien/Chat/Vache. Classe Zoo avec vector<unique_ptr<Animal>>. Bonus : factory creer_animal(espece, nom, age).

📁 exercices/03-poo/05-polymorphisme/

4. STL intermédiaire

16 — Inventaire avec vector

Gestion d'articles : ajouter, retirer, trouver (retour pointeur ou nullptr), valeur totale, tris (lambda comparateurs). Idiome erase-remove.

📁 exercices/04-stl/01-vector-inventaire/

17 — Compteur de mots

Lis du texte sur stdin, normalise (lower + ponctuation), compte avec unordered_map. Tri final par fréquence (et alpha en cas d'égalité).

📁 exercices/04-stl/02-map-mots/

18 — Algorithms STL

10 calculs sans une seule boucle for à la main : accumulate, sort, count_if, transform, copy_if, all_of, minmax_element, médiane.

📁 exercices/04-stl/03-algorithms/

19 — Calculatrice avec exceptions

Hiérarchie d'exceptions custom (CalcError < runtime_error, OperateurInconnu, DivisionParZero). Boucle interactive, catch by const ref.

📁 exercices/04-stl/04-exceptions/

20 — CSV

Lecture/écriture personnes.csv (nom, age, ville). ifstream + getline avec délimiteur ','. Tri par âge. Critique : parsing CSV maison fragile.

📁 exercices/04-stl/05-fichiers/

5. C++ moderne avancé

21 — Graphe avec smart pointers

Modélise un graphe avec shared_ptr<Node> et voisins en weak_ptr pour casser les cycles. Vérifie l'absence de fuite avec ASan sur un cycle A→B→C→A.

📁 exercices/05-modern-cpp/01-smart-pointers/

22 — Event system avec lambdas

Event<Args...> avec subscribe(handler), emit, unsubscribe par token. Démontre captures par valeur et par référence. Attention concurrence pendant emit.

📁 exercices/05-modern-cpp/02-lambdas/

23 — Buffer move-only

Classe gérant un std::byte*. Rule of Five complète : copie supprimée, move noexcept, gérer self-move. Test avec vector<Buffer>.

📁 exercices/05-modern-cpp/03-move/

24 — RAII : Timer + ScopeGuard

ScopedTimer (chrono auto-print à la destruction), ScopeGuard générique avec callback et release(). Destructeurs jamais lançants.

📁 exercices/05-modern-cpp/04-raii/

6. Templates avancé

25 — Fonctions génériques

maximum sur (T, T) et sur initializer_list<T>, moyenne sur Container, spécialisation pour const char*. Bonus C++20 : version concept.

📁 exercices/06-templates/01-fonction-generique/

26 — Stack<T>

Pile générique avec push (lvalue/rvalue), emplace variadic, pop, top, size, empty. Doit marcher avec unique_ptr<T>.

📁 exercices/06-templates/02-stack-template/

27 — Variadic + fold expressions

println, somme, tous_vrais, est_un_de. Comprends les éléments neutres (&& → true, || → false). Bonus : variadic + concept.

📁 exercices/06-templates/03-variadic/

28 — CRTP Comparable

Mixin qui ajoute ==, !=, <, <=, >, >= à toute classe qui définit compare(). Compare avec la version C++20 operator<=> = default qui rend tout ça obsolète.

📁 exercices/06-templates/04-crtp/

7. Concurrence expert

29 — Threads basiques

4 threads avec sleep proportionnel. Synchronise std::cout avec un mutex. Compare avec std::jthread.

📁 exercices/07-concurrence/01-thread/

30 — Producer/Consumer

ConcurrentQueue<T> thread-safe avec mutex + condition_variable. pop() bloquant, done() pour terminer proprement. Évite spurious wakeups avec predicat.

📁 exercices/07-concurrence/02-producer-consumer/

31 — Calcul parallèle async

Somme d'un vector de 10M d'int en parallèle. Découpe en N chunks, std::async(std::launch::async, ...), somme des résultats. Mesure le speedup.

📁 exercices/07-concurrence/03-async/

8. Expert expert

32 — Concepts C++20

Concepts Numeric, Container, Printable. Fonctions contraintes. Compare la qualité des messages d'erreur avec SFINAE.

📁 exercices/08-expert/01-concepts/

33 — Pipeline ranges

Une seule expression : pair, *3, >30, take(3). Bonus : iota infini, zip de deux vecteurs.

📁 exercices/08-expert/02-ranges/

34 — constexpr / consteval

fibonacci, est_premier, génération compile-time des N premiers nombres premiers, hash compile-time. Vérifie avec static_assert.

📁 exercices/08-expert/03-constexpr/

Pour aller plus loin

Lectures que je recommande sans réserve :

  • Effective Modern C++ — Scott Meyers (42 items pour C++11/14)
  • C++ Concurrency in Action — Anthony Williams
  • The C++ Programming Language — Bjarne Stroustrup (le créateur)
  • C++ Core Guidelinesisocpp.github.io/CppCoreGuidelines
  • cppreference.com — la documentation de référence (PAS cplusplus.com qui est obsolète)
  • Compiler Explorer (godbolt.org) — pour voir le code assembleur généré

Spécifique vision & image (IMDS)

  • Learning OpenCV 4 Computer Vision with Python 3 — bon tour d'horizon (les concepts sont indépendants du langage).
  • OpenCV documentationdocs.opencv.org. Très complète.
  • Tesseracttesseract-ocr.github.io.
  • ONNX Runtime C++ — pour déployer du ML cross-framework.
  • Programming Computer Vision with C++ — pour aller plus loin que OpenCV.

Tooling et industriel

  • Modern CMakecliutils.gitlab.io/modern-cmake. Le guide.
  • Professional CMake: A Practical Guide — Craig Scott.
  • spdlog, Google Test, Catch2, Google Benchmark, fmtlib — incontournables.
  • vcpkg et Conan — gestionnaires de paquets.

Ce que tu connais déjà ou faut apprendre vite (entreprise)

  1. Git workflow (branche/PR/rebase) en équipe.
  2. Lire un bug report, reproduire, isoler — la moitié du job.
  3. Lire du code que tu n'as pas écrit — l'autre moitié.
  4. Connaître perf, strace, ldd, nm, objdump au moins de nom.
  5. Bash : suffisamment pour scripter du build/test.
  6. Docker : run/build/exec/logs/inspect.
  7. SQL basique pour les bases métier.
  8. JSON, YAML, TOML — formats config courants.
  9. Notions de réseau : TCP/UDP, RTSP (caméras IP !), HTTP, WebSocket.
  10. Sécurité de base : chiffrement (AES, TLS), hash (SHA-256), pas mettre des secrets dans le code/Git.

Soft skills critiques (vraiment)

  • Documenter tes décisions — qu'est-ce qui a été essayé, pourquoi cette solution.
  • Écrire des PR descriptions claires (pas juste "fix bug").
  • Demander avant de réécrire du code des autres — souvent il y a une raison.
  • Découper tes commits/PR — un grand changeset = review impossible = bug en prod.
  • Estimer ton temps — multiplier par 2 ou 3, surtout sur du legacy.

— Fin du cours —
Bon code. Lance les exercices.