سينجلتون في C++: كيف يعمل تحت الغطاء؟ تحليل السؤال الأسطوري

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

مقدمة: لماذا Singleton ليس مجرد "كائن واحد"



إذا سبق لك أن ألقيت نظرة على Stack Overflow بحثًا عن إجابة لسؤال: "How do you implement the Singleton design pattern?"، فسترى آلاف الأصوات وعشرات الخيارات ونقاشات حامية في التعليقات. أصبح هذا السؤال كلاسيكيًا لأن Singleton هو أحد أسهل الأنماط فهمًا، لكنه من أكثرها خداعًا في التنفيذ الصحيح.



في هذه المقالة، لن نعرض فقط كيفية كتابة Singleton في C++. سننظر تحت الغطاء: سنحلل كيفية عمل الذاكرة الثابتة، وما هي ضمانات التهيئة، ولماذا يمكن للنهج الساذج أن يقتل الأداء، وكيف أصبحت النسخة النهائية (Meyers Singleton) المعيار الفعلي.



إذا كنت تستعد لمقابلة أو تريد فهم C++ بشكل أعمق، فهذه المقالة لك. هيا بنا!



ما هو Singleton؟ تذكير سريع



Singleton (المفرد) هو نمط تصميم إنشائي يضمن أن يكون للفئة مثيل واحد فقط، ويوفر نقطة وصول عالمية إليه.



قد يبدو الأمر بسيطًا، ولكن في C++ هناك تفاصيل دقيقة:

  • أمان الخيوط عند إنشاء الكائن لأول مرة.
  • إدارة دورة الحياة (متى يتم تدمير الكائن؟).
  • منع النسخ والتعيين.
  • مشاكل تعدد الخيوط (data race).


دعنا نستعرض تطور التنفيذ - من الساذج إلى الصناعي.



التنفيذ الساذج: ما المشكلة؟



أبسط طريقة هي جعل المُنشئ خاصًا وتخزين مؤشر ثابت:



class Singleton {public:    static Singleton* getInstance() {        if (instance == nullptr) {            instance = new Singleton();        }        return instance;    }

void doSomething() { // منطق }

private: Singleton() {} // مُنشئ خاص Singleton(const Singleton&) = delete; // منع النسخ Singleton& operator=(const Singleton&) = delete; // منع التعيين

static Singleton* instance;};

// تعريف العضو الثابتSingleton* Singleton::instance = nullptr;


المشاكل:

  1. غير آمن للخيوط. إذا استدعت خيطان getInstance() في نفس الوقت، سيرى كلاهما nullptr وينشئان كائنين. هذا ينتهك ثبات Singleton.
  2. تسرب الذاكرة. الكائن الذي تم إنشاؤه عبر new لا يتم حذفه تلقائيًا. (على الرغم من أن الذاكرة تُعاد لنظام التشغيل عند إنهاء العملية، لكن المُدمّر لا يُستدعى).
  3. مشكلة التهيئة الثابتة. إذا حاول كائن ثابت آخر استخدام Singleton قبل تهيئته - فسيحدث سلوك غير محدد (UB).


يمكن إضافة mutex، لكن هذا سيُبطئ كل استدعاء لـ getInstance()، حتى عندما يكون الكائن قد أُنشئ بالفعل. هذا سيء.



Double-Checked Locking (DCLP): صحيح تقريبًا، لكنه خطير



التحسين الشائع هو فحص المؤشر مرتين: أولاً بدون قفل، ثم مع القفل. يُسمى هذا Double-Checked Locking Pattern.



#include <mutex>

class Singleton {public: static Singleton* getInstance() { if (instance == nullptr) { // الفحص الأول (بدون قفل) std::lock_guard<std::mutex> lock(mutex); if (instance == nullptr) { // الفحص الثاني (مع القفل) instance = new Singleton(); } } return instance; }

private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete;

static Singleton* instance; static std::mutex mutex;};

Singleton* Singleton::instance = nullptr;std::mutex Singleton::mutex;<

المدونات

توصيات الكتب