跳至内容

静态成员、单例与 PIMPL

探索静态成员、Meyers 单例模式、工厂模式以及用于 ABI 稳定性和编译隔离的 PIMPL 惯用法。

超越基本类

当你熟悉了类、构造函数和继承之后,你需要设计模式 ── 针对反复出现的问题的经验证的解决方案。本课涵盖三个重要的模式:单例(受控的全局访问)、工厂(解耦的对象创建)和 PIMPL(编译防火墙和 ABI 稳定性)。我们还会重新审视静态成员及其现代 C++17 内联初始化。

静态成员与 inline static(C++17)

静态成员变量在类的所有实例之间共享 ── 无论存在多少个对象,都只有一个副本。在 C++17 之前,静态成员必须在 .cpp 文件中定义。C++17 的 inline static 允许直接在头文件中定义。

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 单例(线程安全)

Meyers 单例使用函数局部静态变量,C++11 保证其以线程安全的方式初始化。这是在现代 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;
}

工厂模式

工厂模式将对象创建与使用解耦。调用者指定他们想要什么(例如,形状类型),工厂决定实例化哪个具体类。这比在每个创建点使用一连串的 if/else 或 switch 语句更清晰。

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 惯用法(指向实现的指针)

PIMPL(指向实现的指针)惯用法将实现细节隐藏在一个不透明指针后面。优点:(1)更改实现不需要重新编译客户端代码,(2)私有成员不出现在头文件中(ABI 稳定性),(3)更快的编译速度,因为实现头文件只在 .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;
}
常见陷阱

单例本质上是拥有更好接口的全局状态。它们使测试困难(难以重置或模拟),创建隐藏的依赖关系,并使多线程代码复杂化。只有在确实需要单个全局可访问实例时才使用它们 ── 例如硬件驱动程序或应用程序配置。如果你发现自己创建了许多单例,请重新考虑你的设计,改为显式传递依赖项。

最佳实践

对于 ABI 稳定性重要的库接口使用 PIMPL ── 在不使用 PIMPL 的情况下更改私有成员会改变类的布局,破坏二进制兼容性。PIMPL 还能通过隐藏实现头文件大幅减少大型项目的编译时间。代价是每次调用多一次堆分配和一次指针间接寻址。对于性能关键的内循环代码,PIMPL 可能不合适。

关键要点
  • C++17 的 inline static 让你可以在类头文件中直接定义静态成员
  • Meyers 单例使用函数局部静态变量 ── 自 C++11 起线程安全
  • 工厂模式将对象创建与使用解耦,提高可扩展性
  • PIMPL 隐藏实现细节,提供 ABI 稳定性和更快的编译速度
  • 单例是伪装的全局状态 ── 谨慎使用,优先选择依赖注入
  • 在工厂函数上使用 [[nodiscard]] 防止调用者意外忽略创建的对象

测验 — 检验你的知识

(15 经验值)

1. 为什么 Meyers 单例在 C++11 及更高版本中是线程安全的?

2. PIMPL 惯用法的主要好处是什么?

3. 为什么 PIMPL 的析构函数必须在 .cpp 文件中定义,而不是头文件中?