Aller au contenu

Membres statiques, singletons et PIMPL

Explorez les membres statiques, le patron Singleton de Meyers, le patron Factory et l'idiome PIMPL pour la stabilité ABI et l'isolation de compilation.

Au-delà des classes basiques

Une fois que vous êtes à l'aise avec les classes, les constructeurs et l'héritage, vous avez besoin de patrons de conception — des solutions éprouvées à des problèmes récurrents. Cette leçon couvre trois patrons importants : le Singleton (accès global contrôlé), la Factory (création d'objets découplée) et le PIMPL (pare-feu de compilation et stabilité ABI). Nous revisiterons aussi les membres statiques et leur initialisation inline moderne en C++17.

Membres statiques et Inline Static (C++17)

Les variables membres statiques sont partagées entre toutes les instances d'une classe — il n'en existe qu'une seule copie quel que soit le nombre d'objets. Avant C++17, les membres statiques devaient être définis dans un fichier .cpp. L'inline static de C++17 permet la définition directement dans l'en-tête.

static_members.cpp
#include <iostream>
#include <string>
#include <vector>

class Logger {
    // C++17: inline static — no separate .cpp definition needed
    inline static std::vector<std::string> messages_;
    inline static int error_count_ = 0;

public:
    // Static member function — no 'this' pointer, can only
    // access static members
    static void log(const std::string& msg) {
        messages_.push_back(msg);
    }

    static void error(const std::string& msg) {
        messages_.push_back("[ERROR] " + msg);
        ++error_count_;
    }

    static void dump() {
        for (const auto& m : messages_) {
            std::cout << m << '\n';
        }
        std::cout << "Total errors: " << error_count_ << '\n';
    }

    static int error_count() { return error_count_; }
};

int main() {
    Logger::log("Application started");
    Logger::error("File not found");
    Logger::log("Retrying...");
    Logger::error("Permission denied");
    Logger::dump();
    return 0;
}

Singleton de Meyers (thread-safe)

Le Singleton de Meyers utilise une variable statique locale à une fonction, dont C++11 garantit l'initialisation de manière thread-safe. C'est la façon la plus simple et la plus sûre d'implémenter un singleton en C++ moderne.

singleton.cpp
#include <iostream>
#include <string>

class AppConfig {
public:
    // Delete copy and move to prevent duplication
    AppConfig(const AppConfig&) = delete;
    AppConfig& operator=(const AppConfig&) = delete;

    // Meyers Singleton: function-local static
    static AppConfig& instance() {
        static AppConfig config;  // thread-safe since C++11
        return config;
    }

    void set_debug(bool d) { debug_ = d; }
    bool debug() const { return debug_; }

    void set_app_name(const std::string& name) { app_name_ = name; }
    const std::string& app_name() const { return app_name_; }

private:
    // Private constructor — only instance() can create
    AppConfig() : debug_{false}, app_name_{"MyApp"} {}

    bool debug_;
    std::string app_name_;
};

int main() {
    auto& config = AppConfig::instance();
    config.set_app_name("SuperApp");
    config.set_debug(true);

    // Same instance everywhere
    std::cout << AppConfig::instance().app_name() << '\n';
    std::cout << std::boolalpha
              << AppConfig::instance().debug() << '\n';
    return 0;
}

Patron Factory

Le patron Factory découple la création d'objets de leur utilisation. Les appelants spécifient ce qu'ils veulent (par ex., un type de forme), et la factory décide quelle classe concrète instancier. C'est plus propre qu'une chaîne de if/else ou d'instructions switch à chaque site de création.

factory.cpp
#include <iostream>
#include <memory>
#include <string>
#include <stdexcept>

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
    virtual std::string name() const = 0;
};

class Circle : public Shape {
    double r_;
public:
    explicit Circle(double r) : r_{r} {}
    double area() const override { return 3.14159265 * r_ * r_; }
    std::string name() const override { return "Circle"; }
};

class Square : public Shape {
    double side_;
public:
    explicit Square(double s) : side_{s} {}
    double area() const override { return side_ * side_; }
    std::string name() const override { return "Square"; }
};

// Factory function — callers don't need to know concrete types
[[nodiscard]]
std::unique_ptr<Shape> make_shape(const std::string& type, double size) {
    if (type == "circle") return std::make_unique<Circle>(size);
    if (type == "square") return std::make_unique<Square>(size);
    throw std::invalid_argument("Unknown shape: " + type);
}

int main() {
    auto s1 = make_shape("circle", 5.0);
    auto s2 = make_shape("square", 4.0);

    std::cout << s1->name() << ": " << s1->area() << '\n';
    std::cout << s2->name() << ": " << s2->area() << '\n';
    return 0;
}

L'idiome PIMPL (Pointeur to Implementation)

L'idiome PIMPL (Pointeur to Implementation) cache les détails d'implémentation derrière un pointeur opaque. Avantages : (1) changer l'implémentation ne nécessite pas de recompiler les clients, (2) les membres privés n'apparaissent pas dans l'en-tête (stabilité ABI), (3) compilation plus rapide car les en-têtes d'implémentation ne sont nécessaires que dans le fichier .cpp.

pimpl.cpp
// ========== widget.hpp ==========
#include <memory>
#include <string>

class Widget {
public:
    Widget(const std::string& name, int value);
    ~Widget();  // must be declared; defined in .cpp

    // Move operations — defined in .cpp where Impl is complete
    Widget(Widget&&) noexcept;
    Widget& operator=(Widget&&) noexcept;

    // No copy (or implement deep copy in .cpp)
    Widget(const Widget&) = delete;
    Widget& operator=(const Widget&) = delete;

    void do_work() const;
    std::string name() const;

private:
    struct Impl;  // forward declaration only!
    std::unique_ptr<Impl> pimpl_;  // opaque pointer
};

// ========== widget.cpp ==========
// #include "widget.hpp"
#include <iostream>

struct Widget::Impl {
    std::string name;
    int value;
    // Any complex private members go here
    // Changes here do NOT affect the header!
};

Widget::Widget(const std::string& name, int value)
    : pimpl_{std::make_unique<Impl>(Impl{name, value})} {}

Widget::~Widget() = default;  // Impl is complete here
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;

void Widget::do_work() const {
    std::cout << "Working on: " << pimpl_->name
              << " (value=" << pimpl_->value << ")\n";
}

std::string Widget::name() const { return pimpl_->name; }

int main() {
    Widget w{"Engine", 42};
    w.do_work();
    std::cout << w.name() << '\n';
    return 0;
}
Piège

Les singletons sont essentiellement un état global avec une interface plus élégante. Ils rendent les tests difficiles (difficile à réinitialiser ou à simuler), créent des dépendances cachées et compliquent le code multithreadé. Utilisez-les uniquement quand il y a un besoin réel d'une instance unique et globalement accessible — comme un pilote matériel ou la configuration de l'application. Si vous vous retrouvez à créer de nombreux singletons, reconsidérez votre conception et passez les dépendances explicitement à la place.

Bonne pratique

Utilisez PIMPL pour les interfaces de bibliothèque où la stabilité ABI compte — changer les membres privés sans PIMPL change la disposition de la classe, cassant la compatibilité binaire. PIMPL réduit aussi drastiquement les temps de compilation dans les grands projets en cachant les en-têtes d'implémentation. Le coût est une allocation heap supplémentaire et une indirection de pointeur par appel. Pour le code critique en performance dans les boucles internes, PIMPL peut ne pas être approprié.

Points clés
  • L'inline static de C++17 vous permet de définir des membres statiques directement dans l'en-tête de la classe
  • Le Singleton de Meyers utilise une statique locale à une fonction — thread-safe depuis C++11
  • Le patron Factory découple la création d'objets de l'utilisation, améliorant l'extensibilité
  • Le PIMPL cache les détails d'implémentation, fournissant une stabilité ABI et une compilation plus rapide
  • Les singletons sont un état global déguisé — utilisez-les avec parcimonie et préférez l'injection de dépendances
  • [[nodiscard]] sur les fonctions factory empêche les appelants d'ignorer accidentellement l'objet créé

Quiz — Testez vos connaissances

(15 XP)

1. Pourquoi le Singleton de Meyers est-il thread-safe en C++11 et ultérieur ?

2. Quel est le principal avantage de l'idiome PIMPL ?

3. Pourquoi le destructeur PIMPL doit-il être défini dans le fichier .cpp, pas dans l'en-tête ?