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.
#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-Safe)
The Meyers Singleton uses a function-local static variable, which C++11 guarantees is initialized in a thread-safe manner. This is the simplest and safest way to implement a singleton in modern C++.
#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;
}Model de fabrică
Modelul din fabrică decuplează crearea obiectelor de utilizare. Apelanții specifică ceea ce doresc (de exemplu, un tip de formă), iar fabrica decide ce clasă concretă să instanțieze. Acest lucru este mai curat decât un lanț de instrucțiuniif/elsesauswitchla fiecare site de creare.
#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;
}Idiom PIMPL (indicator către implementare)
Expresia PIMPL (Pointer to Implementation) ascunde detaliile de implementare în spatele unui pointer opac. Beneficii: (1) modificarea implementării nu necesită recompilarea clienților, (2) membrii privați nu apar în antet (stabilitatea ABI), (3) compilare mai rapidă, deoarece anteturile de implementare sunt necesare doar în fișierul.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;
}Singleton-urile sunt în esență stare globală cu o interfață mai frumoasă. Acestea fac testarea dificilă (greu de resetat sau batjocorit), creează dependențe ascunse și complică codul cu mai multe fire. Utilizați-le numai atunci când există o nevoie reală pentru o singură instanță accesibilă la nivel global, cum ar fi un driver hardware sau o configurație a aplicației. Dacă vă treziți să creați multe singletonuri, reconsiderați designul și treceți în mod explicit dependențele.
Utilizați PIMPL pentru interfețe de bibliotecă unde stabilitatea ABI contează — schimbarea membrilor privați fără PIMPL schimbă aspectul clasei, rupând compatibilitatea binară. De asemenea, PIMPL reduce drastic timpul de compilare în proiectele mari prin ascunderea antetelor de implementare. Costul este o alocare suplimentară de heap și un indicator indirect pentru fiecare apel. Pentru codul de buclă interioară critică pentru performanță, PIMPL poate să nu fie adecvat.
- C++17
inline staticvă permite să definiți membri statici direct în antetul clasei - Meyers Singleton folosește o funcție statică locală - sigură pentru fire de la C++11
- Modelul din fabrică decuplă crearea obiectelor de utilizare, îmbunătățind extensibilitatea
- PIMPL ascunde detaliile de implementare, oferind stabilitate ABI și compilare mai rapidă
- Singleton-urile sunt o stare globală deghizată - folosește-le cu moderație și preferă injecția de dependență
[[nodiscard]]la funcțiile din fabrică împiedică apelanții să ignore accidental obiectul creat
Test — Verifică-ți cunoștințele
(15 XP)1. De ce este Meyers Singleton sigur pentru fire în C++11 și mai târziu?
2. What is the main benefit of the PIMPL idiom?
3. Why must the PIMPL destructor be defined in the .cpp file, not the header?