Vai al contenuto

Membri Statici, Singleton e PIMPL

Esplora i membri statici, il pattern Singleton di Meyers, il pattern Factory e l'idioma PIMPL per la stabilità dell'ABI e l'isolamento a tempo di compilazione.

Oltre le Classi di Base

Una volta che hai dimestichezza con classi, costruttori ed ereditarietà, ti servono i design pattern — soluzioni collaudate a problemi ricorrenti. Questa lezione copre tre pattern importanti: il Singleton (accesso globale controllato), la Factory (creazione di oggetti disaccoppiata) e PIMPL (barriera di compilazione e stabilità dell'ABI). Riprendiamo anche i membri statici e la loro inizializzazione inline moderna introdotta dal C++17.

Membri Statici e inline static (C++17)

Le variabili membro statiche sono condivise tra tutte le istanze di una classe — ne esiste esattamente una copia, indipendentemente da quanti oggetti ci sono. Prima del C++17 i membri statici dovevano essere definiti in un file .cpp. L'inline static del C++17 permette di definirli direttamente nell'header.

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 di Meyers (Thread-Safe)

Il Singleton di Meyers usa una variabile statica locale a una funzione, che il C++11 garantisce venga inizializzata in modo thread-safe. È il modo più semplice e sicuro di implementare un singleton nel C++ moderno.

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;
}

Pattern Factory

Il pattern Factory disaccoppia la creazione degli oggetti dal loro utilizzo. I chiamanti indicano cosa vogliono (per esempio un tipo di forma) e la factory decide quale classe concreta istanziare. È più pulito di una catena di if/else o di istruzioni switch in ogni punto in cui si creano oggetti.

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;
}

Idioma PIMPL (Pointer to Implementation)

L'idioma PIMPL (Pointer to Implementation) nasconde i dettagli implementativi dietro un puntatore opaco. Vantaggi: (1) cambiare l'implementazione non richiede la ricompilazione del codice client, (2) i membri privati non compaiono nell'header (stabilità dell'ABI), (3) compilazione più veloce, dato che gli header dell'implementazione servono solo nel file .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;
}
Insidia

I singleton sono essenzialmente stato globale con un'interfaccia più elegante. Rendono difficili i test (sono difficili da reinizializzare o da sostituire con dei mock), creano dipendenze nascoste e complicano il codice multi-thread. Usali solo quando serve davvero una singola istanza accessibile globalmente — per esempio un driver hardware o la configurazione dell'applicazione. Se ti accorgi di creare molti singleton, riconsidera il tuo design e passa invece le dipendenze in modo esplicito.

Buona pratica

Usa PIMPL per le interfacce di libreria dove la stabilità dell'ABI è importante — senza PIMPL, cambiare i membri privati cambia il layout della classe, rompendo la compatibilità binaria. PIMPL riduce anche drasticamente i tempi di compilazione nei progetti grandi, nascondendo gli header dell'implementazione. Il costo è un'allocazione sull'heap in più e un'indirezione di puntatore per ogni chiamata. Per codice critico per le prestazioni nei cicli interni, PIMPL potrebbe non essere adatto.

Concetti chiave
  • L'inline static del C++17 ti permette di definire i membri statici direttamente nell'header della classe
  • Il Singleton di Meyers usa una variabile statica locale a una funzione — thread-safe dal C++11
  • Il pattern Factory disaccoppia la creazione degli oggetti dal loro utilizzo, migliorando l'estendibilità
  • PIMPL nasconde i dettagli implementativi, offrendo stabilità dell'ABI e compilazione più veloce
  • I singleton sono stato globale travestito — usali con parsimonia e preferisci la dependency injection
  • [[nodiscard]] sulle funzioni factory impedisce ai chiamanti di ignorare per sbaglio l'oggetto creato

Quiz — Metti alla prova le tue conoscenze

(15 XP)

1. Perché il Singleton di Meyers è thread-safe dal C++11 in poi?

2. Qual è il vantaggio principale dell'idioma PIMPL?

3. Perché il distruttore di una classe PIMPL deve essere definito nel file .cpp e non nell'header?