المُنشئ يستدعي المُنشئ: السلاسل في C++ — هل يمكن ذلك؟

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

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

ابدأ الدورة

«من مُنشئ إلى آخر»: السؤال الأبدي لمطور C++

\n\n

تخيل موقفًا: تكتب كلاسًا بعدة مُنشئات. واحد يستقبل ثلاث معاملات، وآخر اثنين، وثالث واحد. وتتكرر في جميعها نفس منطق التهيئة. تشعر برغبة في كتابة شيء مثل MyClass() : MyClass(42, "default") {} — وتنسى أمر التكرار. لكن مُجمّع C++ (حتى لحظة معينة) ينظر إلى هذا بشكل غير مباشر. هل يمكن أساسًا استدعاء مُنشئ من مُنشئ آخر؟ وإذا كان الجواب نعم — فكيف؟

\n\n

هذا السؤال هو من أكثر الأسئلة شيوعًا على Stack Overflow (عشرات الآلاف من المشاهدات). ليس مستغربًا: موضوع سلاسل المُنشئات (constructor chaining) هو حجر عثرة حقيقي للمبتدئين وحتى المطورين ذوي الخبرة الذين ينتقلون إلى C++ من Java أو Python. في هذه المقالة، سنحلل ثلاثة أساليب لحل المشكلة، من «الحلول المؤقتة» إلى الحلول الأنيقة في C++11، وسنكتشف أي منها يستحق الاستخدام في الكود الحقيقي.

\n\n

لماذا لا يمكن ببساطة استدعاء مُنشئ من مُنشئ؟

\n\n

النهج الساذج يبدو هكذا:

\n\n
class MyClass {\npublic:\n    MyClass() {\n        // كود ما\n    }\n    MyClass(int x) {\n        MyClass(); // خطأ: لا يمكن فعل هذا!\n    }\n};
\n\n

في C++، المُنشئ ليس دالة عادية. فهو يُهيئ الكائن مرة واحدة. إذا حاولت استدعاء مُنشئ آخر بشكل صريح داخل الجسم، سيعتبره المُجمّع إنشاء كائن مؤقت — ولن يتم تهيئة أي شيء في المثيل الحالي. النتيجة: إما خطأ في التجميع، أو (ما هو أسوأ) سلوك غير محدد.

\n\n

قبل معيار C++11، كانت الطريقة القانونية الوحيدة «لمشاركة» منطق التهيئة هي استدعاء دالة خاصة init(). لكن هذا لم يكن تفويضًا حقيقيًا: أعضاء الكلاس كانوا يُهيؤون أولاً بواسطة المُنشئ الافتراضي، ثم يُعاد كتابتهم. غير فعال ومحفوف بالأخطاء.

\n\n

النهج رقم 1: المُنشئات المفوضة (C++11 وما بعده)

\n\n

بدءًا من C++11، ظهرت في اللغة ميزة طال انتظارها — المُنشئات المفوضة (delegating constructors). الآن يمكن استدعاء مُنشئ من مُنشئ آخر مباشرة في قائمة التهيئة:

\n\n
class MyClass {\n    int value;\n    std::string name;\npublic:\n    // المُنشئ الأساسي\n    MyClass(int v, const std::string& n) : value(v), name(n) {\n        std::cout << "تهيئة كاملة: " << value << ", " << name << "\\n";\n    }\n    // المُنشئ المفوض\n    MyClass(int v) : MyClass(v, "default") {}\n    // مُنشئ مفوض آخر\n    MyClass() : MyClass(0, "default") {}\n};
\n\n

كيف يعمل هذا؟ المُنشئ MyClass(int v) يُسلم التحكم إلى المُنشئ MyClass(int, string). هذا الأخير ينفذ كل التهيئة، وبعدها فقط يعود التحكم مرة أخرى. قاعدة مهمة: في قائمة التهيئة لا يمكن خلط استدعاء مُنشئ آخر مع تهيئة مباشرة للأعضاء. إما تفويض، أو تهيئة خاصة — واحد من الاثنين.

\n\n

الإيجابيات والسلبيات

\n
    \n
  • ✅ الأناقة: يصبح الكود نظيفًا، بدون تكرار.
  • \n
  • ✅ الأداء: يُهيأ الكائن مرة واحدة، دون عمليات زائدة.
  • \n
  • ⚠️ القيد: التفويض يعمل فقط في «عمق» واحد — لا يمكن أن تكون السلسلة متكررة (ستتسبب في تجاوز سعة المكدس).
  • \n
  • ⚠️ التوافق: يتطلب C++11 أو أحدث. إذا كنت عالقًا على مُجمّع قديم — هذا الخيار ليس لك.
  • \n
\n\n

النهج رقم 2: دالة init() الخاصة

\n\n

الطريقة الكلاسيكية «ما قبل C++11» — نقل المنطق المشترك إلى دالة خاصة. تبدو هكذا:

\n\n
class MyClass {\n    int value;\n    std::string name;\n    void init(int v, const std::string& n) {\n        value = v;\n        name = n;\n        std::cout << "تم استدعاء init\\n";\n                

المدونات

توصيات الكتب