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.
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
.cppdevient un fichier objet, puis le linker assemble. Les.h/.hppsont 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 ».
0.2 — Le cycle de compilation
Une compilation C++ a quatre phases :
- Préprocesseur — applique
#include,#define,#ifdef. Produit une seule grosse unité de traduction. - Compilation — vérifie la syntaxe, le typage, génère du code objet (
.o). - Édition de liens (linking) — résout les symboles entre fichiers objets et bibliothèques.
- 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
-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.-O2en release.-O0 -gen 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 retournerint. Deux signatures valides :int main()etint main(int argc, char* argv[]).std::cout— flux standard de sortie. Lestd::est l'espace de noms standard.<<— opérateur d'insertion (surchargé pour les flux)."Bonjour, C++ !\n"— chaîne littérale de typeconst char*.\nest un saut de ligne.return 0;— convention POSIX : 0 = succès, autre = erreur.main()peut omettre lereturn, qui vaut alors 0 implicitement (cas unique en C++).
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
| Type | Taille typique | Plage signée | Notes |
|---|---|---|---|
char | 1 octet | -128 à 127 | Signé ou non selon la plateforme (!) |
short | 2 octets | -32 768 à 32 767 | |
int | 4 octets | ±2,1×10⁹ | Le défaut |
long | 4 ou 8 | varie | Évite, préfère les types fixes |
long long | 8 octets | ±9,2×10¹⁸ | |
std::int32_t | exactement 4 | fixe | <cstdint> — préférable |
std::size_t | plateforme | non signé | Pour les tailles/index de containers |
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
bool—true/false. Taille 1 octet en pratique.char— caractère ASCII. Pour Unicode, utilisechar8_t(UTF-8),char16_t,char32_t, ouwchar_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 !)
{} 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
++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";
}
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)) { ... }
for (int i = 0; i < vec.size(); ++i). Trois problèmes :
vec.size()retournestd::size_t(non signé) — comparaison signée/non-signée → warning.- Le compilateur ré-évalue
vec.size()à chaque itération (sauf si optim). - Tu n'as pas besoin de l'index 90% du temps. Utilise
for (auto& x : vec).
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);
}
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, ...
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)
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
* par "pointeur vers". const int* const = "const pointeur vers const int".
- 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.
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 : 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
newsansdelete→ fuite.deletedeux fois → corruption.new[]avecdelete(sans[]) → UB.- Exception entre
newetdelete→ fuite si pas de RAII.
new ni delete à la main. Tu utilises :
std::make_unique<T>(args...)pour la possession uniquestd::make_shared<T>(args...)pour la possession partagéestd::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.
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.
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
};
: 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
};
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
};
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é.
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;
};
- 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
| Mode | public dans base | protected dans base | private dans base |
|---|---|---|---|
: public Base | public | protected | inaccessible |
: protected Base | protected | protected | inaccessible |
: private Base | private | private | inaccessible |
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.
delete pAnimal où pAnimal 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.
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
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
| Container | Caractéristique | Accès | Insertion | Quand utiliser |
|---|---|---|---|---|
vector | Tableau dynamique contigu | O(1) | fin: O(1) amorti | Le défaut. Toujours commencer par ça. |
array | Tableau de taille fixe | O(1) | — | Taille connue à la compilation |
deque | Double-ended queue | O(1) | début/fin: O(1) | Files; insertions aux deux bouts |
list | Liste doublement chaînée | O(n) | O(1) avec iterator | Très rare. Mauvaise localité cache. |
forward_list | Liste simplement chaînée | O(n) | O(1) | Encore plus rare |
map | Arbre rouge-noir trié | O(log n) | O(log n) | Clés ordonnées |
unordered_map | Table de hachage | O(1) moy. | O(1) moy. | Map par défaut si l'ordre n'importe pas |
set / unordered_set | Comme map mais sans valeur | — | — | Unicité, appartenance |
stack / queue / priority_queue | Adapteurs | — | — | Sé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)
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";
}
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) :
- InputIterator — lecture, avance (
++). - OutputIterator — écriture, avance.
- ForwardIterator — multi-passe, lit/écrit.
- BidirectionalIterator —
--en plus. - RandomAccessIterator —
+n,-n,[n],<. - 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
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
- Lance par valeur, attrape par const-référence.
throw MyException{...};etcatch (const MyException& e). - Hérite de
std::exceptionou 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.
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— accompagneshared_ptrpour casser des cycles (ex: parent → enfant en shared, enfant → parent en weak).
- Cycles avec
shared_ptr→ fuite. Utiliseweak_ptrsur 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. Toujoursmake_shared. - Passer
shared_ptrpar valeur copie le compteur (atomique). Préfèreconst shared_ptr&ou même unT*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;
}
};
- Marque move ops
noexcept. Sinonstd::vectorles 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; };
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_; }
};
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;
.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>
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.
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()) { ... }
}};
}
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();
}
- 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.waitpeut se réveiller sans notification. Toujours vérifier la condition dans une boucle (ou via le predicate dewait). - 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();
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);
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; }
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 << ' ';
}
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;
};
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
- Mesure d'abord. Les intuitions sur la perf sont souvent fausses.
perf,callgrind,vtune, microbenchmarks (nanobench, Google Benchmark). - Cache locality > tout. Un
std::vectorbat presque toujoursstd::listet arbres, même pour des opérations "asymptotiquement plus chères". La RAM est la nouvelle disquette. - Évite les allocations dans la boucle chaude.
reserve(), object pools, arena allocators. - Préfère
emplace_backàpush_backpour des objets non triviaux. Évite les copies/moves intermédiaires. - 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. - 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 } - Move semantics — déplace les gros objets, ne les copie pas.
std::vector,std::string, etc., supportent le move depuis C++11. - Inline et constexpr pour les petites fonctions calculables à la compilation.
- Évite les
virtualdans 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. - Profile-Guided Optimization (PGO) et Link-Time Optimization (LTO) : drapeaux
-flto,-fprofile-generate/-fprofile-use.
9.3 — Pièges classiques (à savoir détecter)
| Piège | Description | Solution |
|---|---|---|
| Most Vexing Parse | Widget w(); déclare une fonction ! | Widget w{}; |
| Slicing | Polymorphisme par valeur perd le type dynamique | Référence/pointeur |
| Iterator invalidation | Modifier un container invalide ses itérateurs | Ré-acquérir, indices, copies |
| Self-assignment | x = x dans operator= mal écrit | if (this != &other) ou copy-and-swap |
| Double-free | Pointeur libéré deux fois | Smart pointers |
| Use-after-move | Utiliser un objet déplacé | Discipline + sanitizers |
| Dangling reference/view | Ref ou string_view sur objet détruit | Vé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 UB | i = i++ + ++i | Une seule modification par expression |
| Static init order fiasco | Globals dans des TUs différentes | Singleton Meyers, init explicite |
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
Bases
01 — Hello World 02 — Variables et types 03 — FizzBuzz 04 — Calcul de moyenne 05 — Fibonacci récursif 06 — Min/max d'un tableauMémoire
07 — Swap par pointeurs 08 — Manipulation par référence 09 — Tableau dynamique 10 — Manipulation de stringPOO
11 — Classe Rectangle 12 — Vector3 + opérateurs 13 — Compte bancaire 14 — Hiérarchie Forme 15 — Zoo polymorpheSTL
16 — Inventaire avec vector 17 — Compteur de mots 18 — Algorithms STL 19 — Calculatrice avec exceptions 20 — Lecture CSV10.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.
- 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
| Macro | Effet |
|---|---|
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 / FALSE | Booléen |
EXPECT_DEATH(expr, regex) | expr doit faire crasher (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 Test | Projet sérieux, integration mocking (gMock), grosse boîte, large écosystème |
| Catch2 | Setup rapide, code source moderne, syntaxe expressive, projets moyens |
| doctest | Tests 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());
}
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 :
- Red — écris un test qui échoue (la feature n'existe pas encore).
- Green — écris le minimum de code pour le faire passer.
- 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, oullvm-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.
| Sanitizer | Drapeau | Détecte |
|---|---|---|
| AddressSanitizer (ASan) | -fsanitize=address | Out-of-bounds, use-after-free, double-free, fuites |
| UndefinedBehaviorSanitizer (UBSan) | -fsanitize=undefined | Overflow signé, déréférencement nullptr, shifts invalides… |
| ThreadSanitizer (TSan) | -fsanitize=thread | Races, deadlocks (incompatible avec ASan) |
| MemorySanitizer (MSan) | -fsanitize=memory (Clang) | Lecture de mémoire non initialisée |
| LeakSanitizer (LSan) | inclus dans ASan | Fuites 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
- 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.
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
11.3 — GDB : le debugger
# Compile avec symboles
g++ -g -O0 -std=c++20 prog.cpp -o prog
gdb ./prog
| Commande | Action |
|---|---|
run [args] / r | Lance le programme |
break file.cpp:42 / b | Breakpoint à ligne |
break MaClasse::methode | Breakpoint sur fonction |
continue / c | Reprend l'exécution |
next / n | Step over (ligne suivante) |
step / s | Step into (entre dans la fonction) |
finish | Sort de la fonction courante |
print var / p | Affiche une variable |
watch var | Stop quand var change |
backtrace / bt | Stack trace |
info locals | Variables locales |
frame N / f N | Aller à frame N |
thread N | Bascule 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
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é
- Évite les fuites par design : RAII, smart pointers, containers. La majorité des fuites en C++ moderne viennent de cas non-idiomatiques.
- 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 - Valgrind sur le binaire :
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).valgrind --leak-check=full --show-leak-kinds=all --error-exitcode=1 ./prog - Heaptrack : profileur graphique des allocations.
Voit qui alloue le plus, où, et quand. Excellent pour de la perf mémoire (pas seulement fuites).heaptrack ./prog heaptrack_gui heaptrack.prog.12345.gz - Pression mémoire au runtime : log la
RSSdu processus toutes les N secondes. Si elle augmente linéairement → fuite. Sous Linux : lire/proc/self/status, champVmRSS.
Cas typiques en C++ moderne (oui, ça arrive encore)
- Cycle de
shared_ptr— la cause #1 quand tu as des graphes/observers. Solution :weak_ptrsur un côté. - Capture
thisdans 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'erasejamais (cache sans LRU). - Bibliothèque C qu'on ne libère pas :
cvCreate*(legacy OpenCV C-API),fopensansfclose, FFmpegavcodec_allocsans 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
| Outil | Pour |
|---|---|
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 |
heaptrack | Allocations |
| Google Benchmark | Microbenchmarks reproductibles |
| Tracy | Profiler 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();
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)
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/deletebrut, 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,reserveavant 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,
TODOsans ticket.
- 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);
}
- 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. - LLVM —
VariableName,FunctionName. - Style « C++ moderne » — souvent :
snake_casepour fonctions/variables,PascalCasepour types,UPPER_CASEpour macros,member_oum_memberpour les membres.
Ce qui compte plus que le choix lui-même :
- Cohérence à l'échelle du projet.
- Auto-formatage via
.clang-formatversionné. Personne ne discute du formatage en review. - Header guards uniformes :
#pragma onceest OK partout en pratique, ou guardsPROJET_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 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_TYPE | Sens |
|---|---|
CV_8UC1 | 1 canal, uint8 — image grayscale |
CV_8UC3 | 3 canaux uint8 — image BGR (pas RGB !) |
CV_8UC4 | BGRA |
CV_32FC1 | Float — pour calculs intermédiaires |
CV_32FC3 | Float 3 canaux |
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 :
- Classique : binarisation, segmentation, classification caractère. Tesseract est le leader open source. Bon pour du texte bien posé.
- 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 :
- Conversion grayscale.
- Augmentation de contraste : CLAHE (Contrast Limited Adaptive Histogram Equalization).
- Débruitage :
cv::bilateralFilter(préserve les contours), pas de Gaussian blur agressif. - Binarisation :
cv::adaptiveThresholdou Otsu (cv::THRESH_OTSU). - Désinclinaison (deskew) : projection / Hough.
- Upscaling : Tesseract aime ~30-60 px de hauteur de caractère. Sous-échantillonné = rate.
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
- 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).
- Crop + redressement de la plaque (homographie depuis 4 coins détectés).
- Segmentation des caractères ou reconnaissance directe (CRNN).
- Validation format avec regex pays. Ex France :
^[A-Z]{2}-?\d{3}-?[A-Z]{2}$(SIV) ou ancien FNI. - 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) :
- Détection : où sont les visages ? (Haar, MTCNN, RetinaFace, YOLO-face)
- Alignement : 5 ou 68 landmarks → transformation affine pour normaliser l'orientation.
- Identification :
- Vérification 1:1 — c'est bien Alice ?
- Identification 1:N — qui est cette personne dans une base ?
// 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.
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 :
- Évite les allocations dans la boucle. Pré-alloue tes
cv::Matde travail, réutilise-les.cv::Mat::createne réalloue que si la taille change. - Réduis la résolution avant détection. Détecter sur 640×480 puis projeter sur 4K est 30x plus rapide.
- Threading par étage (cf. plus haut). Mais attention au coût des copies entre threads.
- SIMD : OpenCV exploite SSE/AVX/NEON automatiquement pour ses fonctions. Pour ton code custom, intrinsics ou autovectorisation (
-O3, alignement,std::span). - GPU via OpenCV CUDA, ONNX Runtime, TensorRT. Inférence ML ≥10x plus rapide qu'un CPU.
- Fixed-point au lieu de float quand applicable.
- 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 ...
}
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 Guidelines — isocpp.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 documentation — docs.opencv.org. Très complète.
- Tesseract — tesseract-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 CMake — cliutils.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)
- Git workflow (branche/PR/rebase) en équipe.
- Lire un bug report, reproduire, isoler — la moitié du job.
- Lire du code que tu n'as pas écrit — l'autre moitié.
- Connaître
perf,strace,ldd,nm,objdumpau moins de nom. - Bash : suffisamment pour scripter du build/test.
- Docker : run/build/exec/logs/inspect.
- SQL basique pour les bases métier.
- JSON, YAML, TOML — formats config courants.
- Notions de réseau : TCP/UDP, RTSP (caméras IP !), HTTP, WebSocket.
- 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.