اعضای استاتیک، تکنمونهها و PIMPL
اعضای استاتیک، الگوی تکنمونه مایرز، الگوی کارخانه و اصطلاح PIMPL برای پایداری ABI و عایقبندی زمان کامپایل را کاوش کنید.
فراتر از کلاسهای پایه
وقتی با کلاسها، سازندهها و وراثت راحت شدید، به الگوهای طراحی نیاز دارید — راهحلهای اثباتشده برای مشکلات تکراری. این درس سه الگوی مهم را پوشش میدهد: تکنمونه (Singleton) (دسترسی سراسری کنترلشده)، کارخانه (Factory) (ایجاد اشیاء جداشده) و PIMPL (دیوار آتش کامپایل و پایداری ABI). همچنین اعضای استاتیک و مقداردهی اولیه مدرن inline آنها در C++17 را مرور میکنیم.
اعضای استاتیک و Inline Static (در C++17)
متغیرهای عضو استاتیک بین تمام نمونههای یک کلاس مشترک هستند — دقیقاً یک کپی صرفنظر از تعداد اشیاء موجود وجود دارد. قبل از C++17، اعضای استاتیک باید در یک فایل .cpp تعریف میشدند. inline static در C++17 اجازه تعریف مستقیم در هدر را میدهد.
#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++ مدرن است.
#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 در هر محل ایجاد است.
#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 مورد نیاز هستند.
// ========== 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. تعریف شود، نه در هدر؟