Zum Inhalt springen

Statische Member, Singletons & PIMPL

Erkunde statische Member, das Meyers-Singleton-Muster, das Factory-Muster und das PIMPL-Idiom für ABI-Stabilität und Entkopplung zur Kompilierzeit.

Jenseits einfacher Klassen

Sobald du mit Klassen, Konstruktoren und Vererbung vertraut bist, brauchst du Entwurfsmuster — bewährte Lösungen für wiederkehrende Probleme. Diese Lektion behandelt drei wichtige Muster: das Singleton (kontrollierter globaler Zugriff), die Factory (entkoppelte Objekterzeugung) und PIMPL (Compiler-Firewall und ABI-Stabilität). Außerdem schauen wir uns statische Member und ihre moderne inline-Initialisierung aus C++17 noch einmal an.

Statische Member & inline static (C++17)

Statische Membervariablen werden von allen Instanzen einer Klasse gemeinsam genutzt — es gibt genau eine Kopie, egal wie viele Objekte existieren. Vor C++17 mussten statische Member in einer .cpp-Datei definiert werden. Mit inline static aus C++17 ist die Definition direkt im Header möglich.

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 (thread-sicher)

Das Meyers-Singleton nutzt eine funktionslokale statische Variable, deren Initialisierung C++11 garantiert thread-sicher macht. Das ist der einfachste und sicherste Weg, ein Singleton in modernem C++ umzusetzen.

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

Factory-Muster

Das Factory-Muster entkoppelt die Objekterzeugung von der Verwendung. Aufrufer geben an, was sie wollen (zum Beispiel einen Formtyp), und die Factory entscheidet, welche konkrete Klasse instanziiert wird. Das ist sauberer als eine Kette von if/else- oder switch-Anweisungen an jeder Erzeugungsstelle.

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 (Pointer to Implementation)

Das PIMPL-Idiom (Pointer to Implementation) versteckt Implementierungsdetails hinter einem opaken Zeiger. Die Vorteile: (1) Änderungen an der Implementierung erfordern kein Neukompilieren der Clients, (2) private Member tauchen nicht im Header auf (ABI-Stabilität), (3) schnellere Kompilierung, da die Implementierungs-Header nur in der .cpp-Datei gebraucht werden.

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

Singletons sind im Kern globaler Zustand mit einer freundlicheren Schnittstelle. Sie erschweren das Testen (schwer zurückzusetzen oder zu mocken), erzeugen versteckte Abhängigkeiten und verkomplizieren Multithread-Code. Setze sie nur ein, wenn es wirklich eine einzige, global erreichbare Instanz braucht — etwa bei einem Hardwaretreiber oder der Anwendungskonfiguration. Wenn du merkst, dass du viele Singletons anlegst, überdenke dein Design und übergib Abhängigkeiten lieber explizit.

Best Practice

Verwende PIMPL für Bibliotheksschnittstellen, bei denen ABI-Stabilität zählt — ohne PIMPL verändert jede Änderung an privaten Membern das Klassenlayout und bricht die Binärkompatibilität. PIMPL verkürzt in großen Projekten außerdem die Kompilierzeiten drastisch, weil es Implementierungs-Header verbirgt. Der Preis ist eine zusätzliche Heap-Allokation und eine Zeigerindirektion pro Aufruf. Für performancekritischen Code in inneren Schleifen ist PIMPL deshalb möglicherweise nicht die richtige Wahl.

Wichtigste Erkenntnisse
  • Mit inline static aus C++17 kannst du statische Member direkt im Header der Klasse definieren
  • Das Meyers-Singleton nutzt eine funktionslokale statische Variable — seit C++11 thread-sicher
  • Das Factory-Muster entkoppelt Objekterzeugung von Verwendung und verbessert die Erweiterbarkeit
  • PIMPL versteckt Implementierungsdetails und sorgt für ABI-Stabilität und schnellere Kompilierung
  • Singletons sind verkappter globaler Zustand — setze sie sparsam ein und bevorzuge Dependency Injection
  • [[nodiscard]] an Factory-Funktionen verhindert, dass Aufrufer das erzeugte Objekt versehentlich ignorieren

Quiz — Teste dein Wissen

(15 XP)

1. Warum ist das Meyers-Singleton ab C++11 thread-sicher?

2. Was ist der Hauptvorteil des PIMPL-Idioms?

3. Warum muss der PIMPL-Destruktor in der .cpp-Datei definiert werden und nicht im Header?