C++ Avancé

Niveau: Avancé Durée: 10 heures Programmation

Ce cours approfondit les concepts avancés de C++ essentiels pour le développement d'applications performantes et la programmation système. Vous explorerez les fonctionnalités modernes du C++, les techniques de gestion de mémoire avancées, et les concepts qui font de C++ un langage puissant pour le hacking éthique et la sécurité informatique.

1. Templates et Programmation Générique

Templates de Fonctions

Les templates permettent d'écrire du code générique qui fonctionne avec différents types:

// Template de fonction
template <typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

// Utilisation
int max_int = maximum<int>(10, 20);       // 20
double max_double = maximum(3.14, 2.71);  // 3.14 (déduction de type)

Spécialisation de Templates:

// Template général
template <typename T>
void afficher(T valeur) {
    std::cout << "Valeur: " << valeur << std::endl;
}

// Spécialisation pour les chaînes C
template <>
void afficher<const char*>(const char* valeur) {
    std::cout << "Chaîne: \"" << valeur << "\"" << std::endl;
}

Templates de Classes

Les templates peuvent également être appliqués aux classes:

// Template de classe
template <typename T, int Taille = 10>
class Tableau {
private:
    T elements[Taille];
    
public:
    Tableau() {
        for (int i = 0; i < Taille; i++) {
            elements[i] = T();
        }
    }
    
    T& operator[](int index) {
        if (index < 0 || index >= Taille) {
            throw std::out_of_range("Index hors limites");
        }
        return elements[index];
    }
    
    int taille() const {
        return Taille;
    }
};

// Utilisation
Tableau<int, 5> tableau_entiers;
tableau_entiers[0] = 42;

Tableau<std::string> tableau_chaines;  // Utilise la taille par défaut (10)
tableau_chaines[0] = "Hello";
Note: Les templates sont compilés à la demande. Le compilateur génère le code pour chaque type utilisé.

2. Bibliothèque Standard (STL)

Conteneurs

La STL offre plusieurs types de conteneurs:

Conteneurs séquentiels:

#include <vector>
#include <list>
#include <deque>

// Vector (tableau dynamique)
std::vector<int> nombres = {1, 2, 3, 4, 5};
nombres.push_back(6);  // Ajout à la fin
nombres[0] = 10;       // Accès direct

// List (liste doublement chaînée)
std::list<std::string> noms = {"Alice", "Bob", "Charlie"};
noms.push_front("David");  // Ajout au début
noms.push_back("Eve");     // Ajout à la fin

// Deque (double-ended queue)
std::deque<float> valeurs = {1.1, 2.2, 3.3};
valeurs.push_front(0.0);  // Ajout au début
valeurs.push_back(4.4);   // Ajout à la fin

Conteneurs associatifs:

#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>

// Map (arbre binaire de recherche)
std::map<std::string, int> ages = {
    {"Alice", 25},
    {"Bob", 30},
    {"Charlie", 35}
};
ages["David"] = 40;  // Insertion ou mise à jour

// Set (ensemble ordonné)
std::set<int> nombres_uniques = {3, 1, 4, 1, 5};  // {1, 3, 4, 5}
nombres_uniques.insert(2);  // {1, 2, 3, 4, 5}

// Versions non ordonnées (tables de hachage)
std::unordered_map<std::string, int> scores;
std::unordered_set<int> valeurs_uniques;

Itérateurs

Les itérateurs permettent de parcourir les conteneurs de manière uniforme:

std::vector<int> nombres = {10, 20, 30, 40, 50};

// Parcours avec itérateurs
for (auto it = nombres.begin(); it != nombres.end(); ++it) {
    std::cout << *it << " ";  // 10 20 30 40 50
}

// Itérateurs inversés
for (auto it = nombres.rbegin(); it != nombres.rend(); ++it) {
    std::cout << *it << " ";  // 50 40 30 20 10
}

// For basé sur une plage (C++11)
for (const auto& n : nombres) {
    std::cout << n << " ";  // 10 20 30 40 50
}

Types d'itérateurs:

  • Input: Lecture une seule fois (ex: std::istream_iterator)
  • Output: Écriture une seule fois (ex: std::ostream_iterator)
  • Forward: Lecture multiple, avance uniquement
  • Bidirectional: Avance et recule (ex: std::list)
  • Random Access: Accès direct à n'importe quel élément (ex: std::vector)

Algorithmes

La STL fournit de nombreux algorithmes génériques:

#include <algorithm>
#include <numeric>
#include <vector>

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

// Tri
std::sort(nombres.begin(), nombres.end());  // {1, 2, 3, 5, 8, 9}

// Recherche
auto it = std::find(nombres.begin(), nombres.end(), 5);
if (it != nombres.end()) {
    std::cout << "Trouvé à la position: " << (it - nombres.begin()) << std::endl;
}

// Transformation
std::vector<int> carres(nombres.size());
std::transform(nombres.begin(), nombres.end(), carres.begin(),
               [](int n) { return n * n; });  // {1, 4, 9, 25, 64, 81}

// Réduction
int somme = std::accumulate(nombres.begin(), nombres.end(), 0);  // 28

// Filtrage (avec std::copy_if et back_inserter)
std::vector<int> pairs;
std::copy_if(nombres.begin(), nombres.end(), std::back_inserter(pairs),
             [](int n) { return n % 2 == 0; });  // {2, 8}
Conseil: Préférez les algorithmes de la STL aux boucles manuelles. Ils sont optimisés, testés et expriment clairement l'intention.

3. Gestion Avancée de la Mémoire

Smart Pointers

Les pointeurs intelligents gèrent automatiquement la durée de vie des objets:

#include <memory>

// unique_ptr: propriété exclusive
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
// auto ptr2 = ptr1;  // Erreur: unique_ptr ne peut pas être copié
auto ptr2 = std::move(ptr1);  // Transfert de propriété
// *ptr1 est maintenant indéfini, ptr1 est null

// shared_ptr: propriété partagée avec comptage de références
std::shared_ptr<int> sptr1 = std::make_shared<int>(100);
{
    auto sptr2 = sptr1;  // Compteur de références = 2
    std::cout << *sptr2 << std::endl;  // 100
}  // sptr2 est détruit, compteur = 1
// Quand le compteur atteint 0, la mémoire est libérée

// weak_ptr: référence faible à un shared_ptr
std::weak_ptr<int> wptr = sptr1;
if (auto locked = wptr.lock()) {  // Vérifie si l'objet existe encore
    std::cout << *locked << std::endl;  // 100
}
Attention: Évitez d'utiliser new et delete directement. Préférez les smart pointers pour éviter les fuites de mémoire et les erreurs de gestion.

Allocateurs Personnalisés

Les allocateurs permettent de contrôler précisément l'allocation de mémoire:

template <typename T>
class PoolAllocator {
private:
    // Implémentation d'un pool d'allocation
    
public:
    using value_type = T;
    
    T* allocate(std::size_t n) {
        // Allouer n objets de type T depuis le pool
    }
    
    void deallocate(T* p, std::size_t n) {
        // Libérer la mémoire
    }
    
    template <typename U, typename... Args>
    void construct(U* p, Args&&... args) {
        // Construire un objet à l'adresse p
        new(p) U(std::forward<Args>(args)...);
    }
    
    template <typename U>
    void destroy(U* p) {
        // Détruire un objet sans libérer sa mémoire
        p->~U();
    }
};

// Utilisation avec un conteneur STL
std::vector<int, PoolAllocator<int>> nombres;

Placement New et Alignement

Techniques avancées pour contrôler l'emplacement et l'alignement des objets:

// Placement new: construire un objet à une adresse spécifique
char buffer[sizeof(std::string)];
std::string* str = new(buffer) std::string("Hello");  // Construit dans buffer
str->~string();  // Destruction manuelle (pas de delete)

// Alignement (C++11)
struct alignas(16) AlignedStruct {
    int x;
    char c;
    double d;
};

// Allocation alignée (C++17)
void* ptr = std::aligned_alloc(64, 1024);  // 1024 octets alignés sur 64
// Utilisation...
std::free(ptr);
Applications: Ces techniques sont utiles pour l'optimisation des performances (alignement pour SIMD), les systèmes embarqués avec mémoire limitée, et les structures de données spécialisées.

4. Programmation Concurrente

Threads

C++11 a introduit un support natif pour la programmation multithreads:

#include <thread>
#include <iostream>

void fonction_thread(int id) {
    std::cout << "Thread " << id << " en cours d'exécution" << std::endl;
}

int main() {
    // Création d'un thread
    std::thread t1(fonction_thread, 1);
    
    // Passage d'une lambda
    std::thread t2([](int id) {
        std::cout << "Lambda dans thread " << id << std::endl;
    }, 2);
    
    // Attendre la fin des threads
    t1.join();
    t2.join();
    
    return 0;
}

Synchronisation

Mécanismes pour coordonner l'accès aux ressources partagées:

#include <mutex>
#include <condition_variable>

std::mutex mtx;  // Protège l'accès à une ressource partagée

void fonction_securisee() {
    // Verrouillage manuel
    mtx.lock();
    // Section critique...
    mtx.unlock();
    
    // RAII avec lock_guard (préférable)
    {
        std::lock_guard<std::mutex> lock(mtx);
        // Section critique...
    }  // Déverrouillage automatique
    
    // unique_lock (plus flexible)
    std::unique_lock<std::mutex> lock(mtx);
    // Section critique...
    lock.unlock();  // Déverrouillage explicite
    // ...
    lock.lock();    // Reverrouillage
}

// Condition variable
std::condition_variable cv;
bool pret = false;

void producteur() {
    std::unique_lock<std::mutex> lock(mtx);
    // Préparer les données...
    pret = true;
    cv.notify_one();  // Notifier un thread en attente
}

void consommateur() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return pret; });  // Attendre que pret soit true
    // Traiter les données...
}
Attention: La programmation concurrente est complexe. Les problèmes comme les deadlocks, race conditions et data races peuvent être difficiles à déboguer.

Modèle de Mémoire et Atomiques

Opérations atomiques et ordonnancement de la mémoire:

#include <atomic>

// Variables atomiques
std::atomic<int> compteur(0);
compteur++;  // Opération atomique

// Opérations atomiques explicites
int ancien = compteur.fetch_add(5);  // Ajoute 5 et retourne l'ancienne valeur

// Barrières mémoire
std::atomic_thread_fence(std::memory_order_acquire);  // Barrière d'acquisition
// ...
std::atomic_thread_fence(std::memory_order_release);  // Barrière de libération

// Ordres mémoire
compteur.store(10, std::memory_order_relaxed);  // Ordre relaxé
int val = compteur.load(std::memory_order_acquire);  // Ordre d'acquisition
Note: Le modèle de mémoire C++ permet d'optimiser les performances en relâchant les garanties d'ordonnancement, mais nécessite une compréhension approfondie pour être utilisé correctement.

5. Fonctionnalités Modernes (C++11/14/17/20)

Lambdas et Fonctions

Expressions lambda et fonctionnalités avancées:

#include <functional>

// Lambda de base
auto add = [](int a, int b) { return a + b; };
int sum = add(5, 3);  // 8

// Capture
int multiplier = 10;
auto multiply = [multiplier](int x) { return x * multiplier; };
int result = multiply(5);  // 50

// Capture par référence
auto increment = [&multiplier]() { multiplier++; };
increment();  // multiplier = 11

// Capture par défaut
auto lambda = [=]() { return multiplier; };  // Tout par valeur
auto lambda2 = [&]() { multiplier++; };      // Tout par référence

// Lambda générique (C++14)
auto generic = [](auto x, auto y) { return x + y; };
int sum2 = generic(5, 3);       // 8
double sum3 = generic(3.5, 2.5); // 6.0

// std::function pour stocker des callables
std::function<int(int, int)> func = add;
func = multiply;  // Réassignation

Move Semantics et Perfect Forwarding

Optimisation des transferts d'objets:

// Constructeur de déplacement
class Buffer {
private:
    int* data;
    size_t size;
    
public:
    // Constructeur normal
    Buffer(size_t s) : size(s), data(new int[s]) {}
    
    // Constructeur de copie
    Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
        std::copy(other.data, other.data + size, data);
    }
    
    // Constructeur de déplacement
    Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
        other.data = nullptr;  // Voler les ressources
        other.size = 0;
    }
    
    // Opérateur d'affectation par déplacement
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }
    
    ~Buffer() {
        delete[] data;
    }
};

// Perfect forwarding
template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
Avantage: La sémantique de déplacement permet d'éviter les copies coûteuses en "volant" les ressources d'objets temporaires ou sur le point d'être détruits.

Autres Fonctionnalités Modernes

Aperçu des fonctionnalités récentes:

C++11/14:

// auto et decltype
auto x = 42;
decltype(x) y = x;  // y est de type int

// Range-based for
for (const auto& item : container) { /* ... */ }

// nullptr
void* ptr = nullptr;

// Enum class
enum class Color { Red, Green, Blue };
Color c = Color::Red;

// Initialisation uniforme
std::vector<int> v{1, 2, 3};
struct Point { int x, y; };
Point p{10, 20};

C++17:

// Structured bindings
std::pair<int, std::string> p{42, "hello"};
auto [id, name] = p;

// if constexpr
template <typename T>
void process(T value) {
    if constexpr (std::is_integral_v<T>) {
        // Code pour les types entiers
    } else {
        // Code pour les autres types
    }
}

// std::optional
std::optional<int> maybe_value;
if (maybe_value) {
    int value = *maybe_value;
}

C++20:

// Concepts
template <typename T>
concept Numeric = std::is_arithmetic_v<T>;

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

// Coroutines