Ir al contenido

Miembros Estaticos, Singletons y PIMPL

Explora los miembros estaticos, el patron Singleton de Meyers, el patron Factory, y el idioma PIMPL para estabilidad ABI y aislamiento en tiempo de compilacion.

Mas Alla de las Clases Basicas

Una vez que te sientas comodo con clases, constructores y herencia, necesitas patrones de diseno — soluciones probadas a problemas recurrentes. Esta leccion cubre tres patrones importantes: el Singleton (acceso global controlado), el Factory (creacion de objetos desacoplada) y PIMPL (muro de compilacion y estabilidad ABI). Tambien revisitamos los miembros estaticos y su inicializacion inline moderna de C++17.

Miembros Estaticos e Inline Static (C++17)

Las variables miembro estaticas se comparten entre todas las instancias de una clase — hay exactamente una copia independientemente de cuantos objetos existan. Antes de C++17, los miembros estaticos tenian que definirse en un archivo .cpp. El inline static de C++17 permite la definicion directamente en el 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 de Meyers (Thread-Safe)

El Singleton de Meyers usa una variable estatica local de funcion, que C++11 garantiza que se inicializa de manera segura para hilos. Esta es la forma mas simple y segura de implementar un singleton en 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;
}

Patron Factory

El patron Factory desacopla la creacion de objetos de su uso. Los llamadores especifican lo que quieren (por ejemplo, un tipo de forma), y la fabrica decide que clase concreta instanciar. Esto es mas limpio que una cadena de sentencias if/else o switch en cada sitio de creacion.

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 (Puntero a Implementacion)

El idioma PIMPL (Puntero a Implementacion) oculta los detalles de implementacion detras de un puntero opaco. Beneficios: (1) cambiar la implementacion no requiere recompilar los clientes, (2) los miembros privados no aparecen en el header (estabilidad ABI), (3) compilacion mas rapida ya que los headers de implementacion solo se necesitan en el archivo .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;
}
Error común

Los singletons son esencialmente estado global con una interfaz mas bonita. Hacen las pruebas dificiles (dificil de reiniciar o simular), crean dependencias ocultas y complican el codigo multihilo. Usalos solo cuando haya una necesidad genuina de una unica instancia accesible globalmente — como un controlador de hardware o configuracion de la aplicacion. Si te encuentras creando muchos singletons, reconsidera tu diseno y pasa las dependencias explicitamente en su lugar.

Buena práctica

Usa PIMPL para interfaces de biblioteca donde la estabilidad ABI importa — cambiar los miembros privados sin PIMPL cambia el layout de la clase, rompiendo la compatibilidad binaria. PIMPL tambien reduce drasticamente los tiempos de compilacion en proyectos grandes al ocultar los headers de implementacion. El costo es una asignacion extra en el monton y una indireccion de puntero por llamada. Para codigo critico en rendimiento de bucles internos, PIMPL puede no ser apropiado.

Puntos clave
  • El inline static de C++17 permite definir miembros estaticos directamente en el header de la clase
  • El Singleton de Meyers usa una estatica local de funcion — seguro para hilos desde C++11
  • El patron Factory desacopla la creacion de objetos del uso, mejorando la extensibilidad
  • PIMPL oculta detalles de implementacion, proporcionando estabilidad ABI y compilacion mas rapida
  • Los singletons son estado global disfrazado — usalos con moderacion y prefiere la inyeccion de dependencias
  • [[nodiscard]] en funciones factory previene que los llamadores ignoren accidentalmente el objeto creado

Quiz — Pon a prueba tus conocimientos

(15 XP)

1. Por que el Singleton de Meyers es seguro para hilos en C++11 y posteriores?

2. Cual es el principal beneficio del idioma PIMPL?

3. Por que el destructor PIMPL debe definirse en el archivo .cpp, no en el header?