Přeskočit na obsah

Static Members, Singletons & PIMPL

Explore static members, the Meyers Singleton pattern, the Factory pattern, and the PIMPL idiom for ABI stability and compile-time insulation.

Beyond Basic Classes

Once you are comfortable with classes, constructors, and inheritance, you need design patterns — proven solutions to recurring problems. This lesson covers three important patterns: the Singleton (controlled global access), the Factory (decoupled object creation), and PIMPL (compilation firewall and ABI stability). We also revisit static members and their modern C++17 inline initialization.

Static Members & Inline Static (C++17)

Static member variables are shared across all instances of a class — there is exactly one copy regardless of how many objects exist. Before C++17, static members had to be defined in a.cppfile. C++17's inline static allows definition directly in the 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;
}

Meyers Singleton (bezpečné pro vlákna)

Meyers Singleton používá funkční lokální statickou proměnnou, u které C++11 zaručuje, že je inicializována způsobem bezpečným pro vlákna. Toto je nejjednodušší a nejbezpečnější způsob implementace singletonu v moderním C++.

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

Tovární vzor

Tovární vzor odděluje vytváření objektů od použití. Volající specifikují, co chtějí (např. typ tvaru), a továrna rozhodne, kterou konkrétní třídu vytvoří. Je to čistší než řetězec příkazůif/elseneboswitchna každém místě tvorby.

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

PIMPL Idiom (ukazatel na implementaci)

Idioma PIMPL (Pointer to Implementation) skrývá detaily implementace za neprůhledným ukazatelem. Výhody: (1) změna implementace nevyžaduje rekompilaci klientů, (2) soukromí členové se neobjevují v hlavičce (stabilita ABI), (3) rychlejší kompilace, protože hlavičky implementace jsou potřeba pouze v souboru.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;
}
Úskalí

Singletons are essentially global state with a nicer interface. They make testing difficult (hard to reset or mock), create hidden dependencies, and complicate multi-threaded code. Use them only when there is a genuine need for a single, globally accessible instance — such as a hardware driver or application configuration. If you find yourself creating many singletons, reconsider your design and pass dependencies explicitly instead.

Osvědčený postup

Použijte PIMPL pro rozhraní knihoven, kde záleží na stabilitě ABI – změna soukromých členů bez PIMPL změní rozložení třídy a naruší binární kompatibilitu. PIMPL také drasticky zkracuje dobu kompilace ve velkých projektech tím, že skrývá implementační hlavičky. Cena je jedno přidělení haldy navíc a jedno směrování ukazatele na volání. Pro kód vnitřní smyčky kritický pro výkon nemusí být PIMPL vhodný.

Klíčové poznatky
  • C++17inline staticlets you define static members directly in the class header
  • Meyers Singleton používá funkční lokální statický náboj – bezpečný pro vlákna od C++11
  • The Factory pattern decouples object creation from usage, improving extensibility
  • PIMPL hides implementation details, providing ABI stability and faster compilation
  • Singletony jsou maskovaným globálním státem – používejte je střídmě a preferujte vkládání závislostí
  • [[nodiscard]]on factory functions prevents callers from accidentally ignoring the created object

Kvíz — Otestujte své znalosti

(15 XP)

1. Why is the Meyers Singleton thread-safe in C++11 and later?

2. Jaká je hlavní výhoda idiomu PIMPL?

3. Proč musí být destruktor PIMPL definován v souboru .cpp, nikoli v záhlaví?