رفتن به محتوا

اعضای استاتیک، تک‌نمونه‌ها و PIMPL

اعضای استاتیک، الگوی تک‌نمونه مایرز، الگوی کارخانه و اصطلاح PIMPL برای پایداری ABI و عایق‌بندی زمان کامپایل را کاوش کنید.

فراتر از کلاس‌های پایه

وقتی با کلاس‌ها، سازنده‌ها و وراثت راحت شدید، به الگوهای طراحی نیاز دارید — راه‌حل‌های اثبات‌شده برای مشکلات تکراری. این درس سه الگوی مهم را پوشش می‌دهد: تک‌نمونه (Singleton) (دسترسی سراسری کنترل‌شده)، کارخانه (Factory) (ایجاد اشیاء جداشده) و PIMPL (دیوار آتش کامپایل و پایداری ABI). همچنین اعضای استاتیک و مقداردهی اولیه مدرن inline آن‌ها در ‎C++‎17 را مرور می‌کنیم.

اعضای استاتیک و Inline Static (در ‎C++‎17)

متغیرهای عضو استاتیک بین تمام نمونه‌های یک کلاس مشترک هستند — دقیقاً یک کپی صرف‌نظر از تعداد اشیاء موجود وجود دارد. قبل از ‎C++‎17، اعضای استاتیک باید در یک فایل .cpp تعریف می‌شدند. inline static در ‎C++‎17 اجازه تعریف مستقیم در هدر را می‌دهد.

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) از یک متغیر استاتیک محلی تابع استفاده می‌کند که ‎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;
}

الگوی کارخانه (Factory Pattern)

الگوی کارخانه ایجاد اشیاء را از استفاده جدا می‌کند. فراخوانی‌کنندگان مشخص می‌کنند چه می‌خواهند (مثلاً نوع شکل)، و کارخانه تصمیم می‌گیرد کدام کلاس مشخص را نمونه‌سازی کند. این تمیزتر از زنجیره‌ای از دستورات 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 (Pointer to Implementation) جزئیات پیاده‌سازی را پشت یک اشاره‌گر مبهم پنهان می‌کند. مزایا: (۱) تغییر پیاده‌سازی نیازی به کامپایل مجدد کلاینت‌ها ندارد، (۲) اعضای خصوصی در هدر ظاهر نمی‌شوند (پایداری ABI)، (۳) کامپایل سریع‌تر زیرا هدرهای پیاده‌سازی فقط در فایل .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;
}
دام

تک‌نمونه‌ها اساساً وضعیت سراسری با رابطی بهتر هستند. آن‌ها تست را دشوار می‌کنند (بازنشانی یا شبیه‌سازی دشوار)، وابستگی‌های پنهان ایجاد می‌کنند و کد چندنخی را پیچیده می‌کنند. فقط زمانی از آن‌ها استفاده کنید که نیاز واقعی به یک نمونه واحد و قابل دسترسی سراسری وجود دارد — مانند درایور سخت‌افزار یا پیکربندی برنامه. اگر متوجه شدید تک‌نمونه‌های زیادی ایجاد می‌کنید، طراحی خود را بازبینی کنید و وابستگی‌ها را به‌صورت صریح ارسال کنید.

بهترین شیوه

از PIMPL برای رابط‌های کتابخانه استفاده کنید جایی که پایداری ABI اهمیت دارد — تغییر اعضای خصوصی بدون PIMPL چیدمان کلاس را تغییر می‌دهد و سازگاری دودویی را می‌شکند. PIMPL همچنین زمان کامپایل را در پروژه‌های بزرگ به‌طور چشمگیری کاهش می‌دهد با پنهان کردن هدرهای پیاده‌سازی. هزینه آن یک تخصیص هیپ اضافی و یک غیرمستقیم اشاره‌گر برای هر فراخوانی است. برای کد حساس به عملکرد در حلقه‌های داخلی، PIMPL ممکن است مناسب نباشد.

نکات کلیدی
  • inline static در ‎C++‎17 به شما اجازه می‌دهد اعضای استاتیک را مستقیماً در هدر کلاس تعریف کنید
  • تک‌نمونه مایرز از یک استاتیک محلی تابع استفاده می‌کند — از ‎C++‎11 ایمن در برابر نخ
  • الگوی کارخانه ایجاد اشیاء را از استفاده جدا می‌کند و قابلیت گسترش را بهبود می‌بخشد
  • PIMPL جزئیات پیاده‌سازی را پنهان می‌کند و پایداری ABI و کامپایل سریع‌تر فراهم می‌کند
  • تک‌نمونه‌ها وضعیت سراسری در لباس مبدل هستند — با احتیاط استفاده کنید و تزریق وابستگی را ترجیح دهید
  • [[nodiscard]] روی توابع کارخانه از نادیده گرفتن تصادفی شیء ایجادشده توسط فراخوانی‌کنندگان جلوگیری می‌کند

آزمون — دانش خود را بسنجید

(15 امتیاز)

1. چرا تک‌نمونه مایرز در ‎C++‎11 و بعد از آن ایمن در برابر نخ است؟

2. مزیت اصلی اصطلاح PIMPL چیست؟

3. چرا مخرب PIMPL باید در فایل cpp. تعریف شود، نه در هدر؟