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 .cpp file. 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;
}Factory Pattern
The Factory pattern decouples object creation from usage. Callers specify what they want (e.g., a shape type), and the factory decides which concrete class to instantiate. This is cleaner than a chain of if/else or switch statements at every creation site.
#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)
The PIMPL (Pointer to Implementation) idiom hides implementation details behind an opaque pointer. Benefits: (1) changing the implementation does not require recompiling clients, (2) private members do not appear in the header (ABI stability), (3) faster compilation since implementation headers are only needed in the .cpp file.
// ========== 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;
}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.
Use PIMPL for library interfaces where ABI stability matters — changing private members without PIMPL changes the class layout, breaking binary compatibility. PIMPL also drastically reduces compilation times in large projects by hiding implementation headers. The cost is one extra heap allocation and one pointer indirection per call. For performance-critical inner-loop code, PIMPL may not be appropriate.
- C++17
inline staticlets you define static members directly in the class header - The Meyers Singleton uses a function-local static — thread-safe since C++11
- The Factory pattern decouples object creation from usage, improving extensibility
- PIMPL hides implementation details, providing ABI stability and faster compilation
- Singletons are global state in disguise — use them sparingly and prefer dependency injection
[[nodiscard]]on factory functions prevents callers from accidentally ignoring the created object
Quiz — Test Your Knowledge
(15 XP)1. Why is the Meyers Singleton thread-safe in C++11 and later?
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?