الأعضاء الثابتون والمفردون وPIMPL
استكشف الأعضاء الثابتين، ونمط Meyers Singleton، ونمط المصنع، ومصطلح PIMPL لاستقرار ABI وعزل وقت الترجمة.
ما وراء الطبقات الأساسية
بمجرد أن تعتاد على الفئات والمنشئات والميراث، فإنك تحتاج إلى أنماط تصميم - حلول مثبتة للمشكلات المتكررة. يغطي هذا الدرس ثلاثة أنماط مهمة: Singleton (الوصول الشامل المتحكم فيه)، وFactory (إنشاء كائن منفصل)، وPIMPL (جدار الحماية التجميعي واستقرار ABI). نعيد أيضًا النظر في الأعضاء الثابتين والتهيئة المضمنة الحديثة لـ C++17.
الأعضاء الثابتون والثابت المضمّن (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;
}نمط المصنع
نمط المصنع يفصل بين إنشاء الكائن والاستخدام. يحدد المتصلون ما يريدون (على سبيل المثال، نوع الشكل)، ويقرر المصنع الفئة الملموسة التي سيتم إنشاء مثيل لها. يعد هذا أنظف من سلسلة عبارات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 (مؤشر التنفيذ) تفاصيل التنفيذ خلف مؤشر غير شفاف. الفوائد: (1) تغيير التنفيذ لا يتطلب إعادة ترجمة العملاء، (2) لا يظهر الأعضاء الخاصون في الرأس (استقرار ABI)، (3) تجميع أسرع نظرًا لأن رؤوس التنفيذ مطلوبة فقط في ملف.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;
}تعد الأغاني المنفردة في الأساس حالة عالمية ذات واجهة أجمل. إنها تجعل الاختبار صعبًا (يصعب إعادة ضبطه أو الاستهزاء به)، وتنشئ تبعيات مخفية، وتعقد التعليمات البرمجية متعددة الخيوط. استخدمها فقط عندما تكون هناك حاجة حقيقية لمثيل واحد يمكن الوصول إليه عالميًا - مثل برنامج تشغيل الأجهزة أو تكوين التطبيق. إذا وجدت نفسك تقوم بإنشاء العديد من المفردات، فأعد النظر في تصميمك وقم بتمرير التبعيات بشكل صريح بدلاً من ذلك.
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
اختبار — اختبر معرفتك
(15 نقطة خبرة)1. Why is the Meyers Singleton thread-safe in C++11 and later?
2. ما هي الفائدة الرئيسية من لغة PIMPL؟
3. لماذا يجب تعريف أداة تدمير PIMPL في ملف .cpp، وليس في الرأس؟