تُستخدم فئات الواجهة على نطاق واسع في برامج C ++. ولكن ، لسوء الحظ ، غالبًا ما يتم ارتكاب الأخطاء عند تنفيذ الحلول بناءً على فئات الواجهة. تصف المقالة كيفية تصميم فئات الواجهة بشكل صحيح ؛ يتم النظر في العديد من الخيارات. يتم وصف استخدام المؤشرات الذكية بالتفصيل. يتم إعطاء مثال على تطبيق فئة استثناء ونموذج فئة المجموعة بناءً على فئات الواجهة.
جدول المحتويات
مقدمة
فئة الواجهة هي فئة لا تحتوي على بيانات وتتكون بشكل أساسي من وظائف افتراضية بحتة. يسمح لك هذا الحل بفصل التنفيذ تمامًا عن الواجهة - يستخدم العميل فئة الواجهة - في مكان آخر يتم إنشاء فئة مشتقة يتم فيها إعادة تعريف الوظائف الافتراضية البحتة وتحديد وظيفة المصنع. تفاصيل التنفيذ مخفية تمامًا عن العميل. بهذه الطريقة ، يتم تنفيذ التغليف الحقيقي ، وهو أمر مستحيل مع الطبقة المعتادة. يمكنك أن تقرأ عن فئات الواجهة من Scott Meyers [Meyers2]. تسمى فئات الواجهة أيضًا فئات البروتوكول.
يتيح لك استخدام فئات الواجهة إضعاف التبعيات بين أجزاء مختلفة من المشروع ، مما يبسط تطوير الفريق ويقلل وقت التجميع / التجميع. تسهل فئات الواجهة تنفيذ حلول ديناميكية ومرنة عند تحميل الوحدات بشكل انتقائي في وقت التشغيل. يؤدي استخدام فئات الواجهة كمكتبة واجهة (API) (SDK) إلى تبسيط حل مشكلات التوافق الثنائية.
يتم استخدام فئات الواجهات على نطاق واسع ، بمساعدتها على تنفيذ واجهة (API) للمكتبات (SDK) ، وواجهة المكونات الإضافية (الإضافات) وأكثر من ذلك بكثير. يتم تنفيذ العديد من أنماط عصابة الأربعة بشكل طبيعي باستخدام فئات الواجهة. تتضمن فئات الواجهة واجهات COM. ولكن ، لسوء الحظ ، غالبًا ما يتم ارتكاب الأخطاء عند تنفيذ الحلول بناءً على فئات الواجهة. دعونا نحاول توضيح هذه المشكلة.
1. وظائف الأعضاء الخاصة ، وإنشاء وحذف الكائنات
يصف هذا القسم بإيجاز عددًا من ميزات C ++ التي تحتاج إلى معرفتها لفهم الحلول المقدمة لفئات الواجهة بشكل كامل.
1.1. وظائف الأعضاء الخاصة
إذا لم يحدد المبرمج وظائف أعضاء الفئة من القائمة التالية - المنشئ الافتراضي ، أو منشئ النسخ ، أو عامل تعيين النسخ ، أو المدمر - عندها يمكن للمترجم القيام بذلك نيابة عنه. أضاف C ++ 11 مُنشئ نقل وعامل تعيين نقل إلى هذه القائمة. تسمى وظائف الأعضاء هذه وظائف الأعضاء الخاصة. يتم إنشاؤها فقط إذا تم استخدامها ، ويتم استيفاء الشروط الإضافية الخاصة بكل وظيفة. نلفت الانتباه إلى حقيقة أن هذا الاستخدام قد يتبين أنه مخفي تمامًا (على سبيل المثال ، عند تنفيذ الوراثة). إذا تعذر إنشاء الوظيفة المطلوبة ، فسيتم إنشاء خطأ. (باستثناء عمليات النقل ، يتم استبدالها بعمليات النسخ.) وظائف الأعضاء التي تم إنشاؤها بواسطة المترجم عامة ويمكن تضمينها.
لا يتم توارث وظائف الأعضاء الخاصة ، إذا كانت هناك حاجة إلى وظيفة عضو خاص في الفئة المشتقة ، سيحاول المترجم دائمًا توليدها ؛ ولا يؤثر وجود وظيفة العضو المقابلة المحددة في الفئة الأساسية من قبل المبرمج على ذلك.
يمكن للمبرمج منع إنشاء وظائف الأعضاء الخاصة ، في C ++ 11 من الضروري استخدام بناء "=delete"
عند الإعلان ، في C ++ 98 إعلان وظيفة العضو المقابلة الخاصة وعدم تعريفها. في وراثة الصف ، ينطبق حظر إنشاء وظيفة عضو خاص في الفئة الأساسية على جميع الفئات المشتقة.
إذا كان المبرمج مرتاحًا لوظائف الأعضاء التي تم إنشاؤها بواسطة المترجم ، فيمكنه في C ++ 11 الإشارة إلى ذلك بشكل صريح ، وليس فقط إسقاط الإعلان. للقيام بذلك ، يجب عليك استخدام بناء "=default"
عند الإعلان ، في حين أن قراءة التعليمات البرمجية بشكل أفضل وظهور ميزات إضافية تتعلق بإدارة مستوى الوصول.
يمكن الاطلاع على تفاصيل وظائف الأعضاء الخاصة في [Meyers3].
1.2. إنشاء وحذف الكائنات - التفاصيل الأساسية
يعد إنشاء وحذف الكائنات باستخدام عوامل التشغيل new/delete
عملية نموذجية في واحد. عند استدعاء new
، يتم تخصيص الذاكرة أولاً للكائن. إذا كان التحديد ناجحًا ، يتم استدعاء المنشئ. إذا قام المُنشئ بطرح استثناء ، فسيتم تحرير الذاكرة المخصصة. عندما يتم استدعاء عامل delete
، يحدث كل شيء في الترتيب العكسي: أولاً ، يتم استدعاء المدمر ، ثم يتم تحرير الذاكرة. يجب ألا يدمر المدمر الاستثناءات.
إذا تم استخدام عامل التشغيل new
لإنشاء صفيف من الكائنات ، فسيتم أولاً تخصيص الذاكرة للصفيف بأكمله. إذا كان التحديد ناجحًا ، فسيتم استدعاء المُنشئ الافتراضي لكل عنصر في المصفوفة بدءًا من الصفر. إذا قام أي مُنشئ بطرح استثناء ، فسيتم استدعاء المدمر بالترتيب العكسي لاستدعاء المُنشئ لكل العناصر التي تم إنشاؤها للصفيف ، ثم يتم تحرير الذاكرة المخصصة. لحذف صفيف ، يجب استدعاء عامل delete[]
(يسمى عامل delete
للصفائف) ، وبالنسبة لجميع عناصر المصفوفة ، يتم استدعاء المدمر بالترتيب العكسي لاستدعاء المُنشئ ، ثم يتم تحرير الذاكرة المخصصة.
إنتباه! يجب استدعاء الشكل الصحيح لعامل delete
، اعتمادًا على ما إذا تم حذف كائن واحد أو صفيف. يجب مراعاة هذه القاعدة بشكل صارم ، وإلا يمكنك الحصول على سلوك غير محدد ، أي أنه يمكن أن يحدث أي شيء: تسرب الذاكرة ، العطل ، إلخ. راجع [Meyers2] للحصول على التفاصيل.
std::bad_alloc
وظائف تخصيص الذاكرة القياسية في تلبية الطلب من خلال استثناء نوع std::bad_alloc
.
من الآمن تطبيق أي شكل من أشكال عامل delete
على مؤشر فارغ.
في الوصف أعلاه ، من الضروري توضيح واحد. بالنسبة إلى ما يسمى بالأنواع التافهة (الأنواع المدمجة ، الهياكل على غرار C) ، قد لا يتم استدعاء المنشئ ، ولا يفعل المدمر شيئًا على أي حال. انظر أيضًا القسم 1.6.
1.3. مستوى الوصول المدمر
عند تطبيق عامل delete
على مؤشر على فئة ، يجب أن يكون مدمر تلك الفئة متاحًا في نقطة استدعاء delete
. (هناك بعض الاستثناءات لهذه القاعدة ، التي تمت مناقشتها في القسم 1.6.) وبالتالي ، من خلال جعل المدمر آمنًا أو مغلقًا ، يحظر المبرمج استخدام عامل delete
حيث لا يكون المدمر متاحًا. تذكر أنه إذا لم يتم تعريف المدمر في الفئة ، فإن المترجم سيقوم بذلك من تلقاء نفسه ، وسوف يكون هذا المدمر مفتوحًا (انظر القسم 1.1).
1.4. إنشاء وحذف في وحدة واحدة
إذا قام عامل التشغيل new
بإنشاء كائن ، فيجب أن يكون عامل delete
في نفس الوحدة النمطية delete
. بالمعنى المجازي ، "ضعها في مكانها". هذه القاعدة معروفة جيدًا ؛ انظر على سبيل المثال ، [Sutter / Alexandrescu]. إذا تم انتهاك هذه القاعدة ، فقد يحدث "عدم تطابق" وظائف تخصيص وتحرير الذاكرة ، مما يؤدي ، كقاعدة عامة ، إلى إنهاء غير طبيعي للبرنامج.
1.5. الحذف متعدد الأشكال
إذا كنت تقوم بتصميم تسلسل هرمي متعدد الأشكال للفئات التي تم حذف مثيلاتها باستخدام عامل delete
، فيجب أن يكون هناك مدمر افتراضي مفتوح في الفئة الأساسية ، وهذا يضمن أن يتم استدعاء المدمر للنوع الفعلي للكائن عند تطبيق عامل delete
على المؤشر على الفئة الأساسية. إذا تم انتهاك هذه القاعدة ، قد يحدث استدعاء لمدمّر الفئة الأساسية ، مما قد يؤدي إلى تسرب الموارد.
1.6. الحذف عندما يكون تصريح الفصل الدراسي غير مكتمل
يمكن أن تثير عامل عامل delete
النهمة مشاكل معينة ؛ يمكن تطبيقها على مؤشر من نوع void*
أو على مؤشر لفئة لديها تصريح غير مكتمل (استباقي). في هذه الحالة ، لا يحدث خطأ ، يتم تخطي استدعاء المدمر فقط ، فقط وظيفة تحرير الذاكرة تسمى. فكر في مثال:
class X;
يتم تجميع هذا الرمز حتى في حالة عدم توفر تعريف فئة X
بالكامل عند نظير قرص delete
. صحيح ، عند تجميع (Visual Studio) يتم إصدار تحذير:
warning C4150: deletion of pointer to incomplete type 'X'; no destructor called
إذا كان هناك تطبيق لـ X
و CreateX()
، فسيتم CreateX()
التعليمات البرمجية ، إذا قام CreateX()
بإرجاع مؤشر إلى الكائن الذي تم إنشاؤه بواسطة عامل التشغيل new
، فعندئذ يتم تنفيذ المكالمة Foo()
بنجاح ، لا يتم استدعاء المدمر. من الواضح أن هذا يمكن أن يؤدي إلى استنزاف الموارد ، لذلك مرة أخرى حول الحاجة إلى توخي الحذر بشأن التحذيرات.
هذا الوضع ليس بعيد المنال ، ويمكن أن ينشأ بسهولة عند استخدام فئات مثل المؤشرات الذكية أو فئات الواصف. سكوت مايرز يتعامل مع هذه القضية في [Meyers3].
2. وظائف افتراضية بحتة وفصول مجردة
يعتمد مفهوم فئات الواجهة على مفاهيم C ++ مثل الوظائف الافتراضية البحتة والفئات المجردة.
2.1. وظائف افتراضية خالصة
وظيفة افتراضية تم تعريفها باستخدام بناء "=0"
تسمى محض ظاهري.
class X {
على عكس وظيفة افتراضية عادية ، لا يمكن تحديد وظيفة افتراضية بحتة (باستثناء المدمر ، انظر القسم 2.3) ، ولكن يجب إعادة تعريفها في إحدى الفئات المشتقة.
يمكن تحديد الوظائف الافتراضية البحتة. يوفر Emblem Sutter العديد من الاستخدامات المفيدة لهذه الميزة [Shutter].
2.2. دروس مجردة
الفئة المجردة هي فئة لها وظيفة ظاهرية واحدة على الأقل. ستكون الفئة المشتقة من فئة مجردة ولا تتجاوز وظيفة افتراضية بحتة واحدة على الأقل مجردة أيضًا. يمنع معيار C ++ إنشاء مثيلات فئة مجردة ؛ يمكنك فقط إنشاء مثيلات مشتقات من فئات غير مجردة. وبالتالي ، يتم إنشاء فئة مجردة لاستخدامها كفئة أساسية. وفقًا لذلك ، إذا تم تعريف المنشئ في فئة مجردة ، فلا معنى لجعله مفتوحًا ، يجب حمايته.
2.3. المدمر الظاهري الصرفة
في بعض الحالات ، يُنصح بعمل مدمر افتراضي خالص. لكن هذا الحل له ميزتان.
- يجب تحديد المدمر الظاهري البحت. (يتم استخدام التعريف الافتراضي عادةً ، أي استخدام البناء
"=default"
.) يستدعي مُدمر الفئة المشتق مُدمِّرات الفئة الأساسية على طول سلسلة الميراث بالكامل ، وبالتالي ، فإن قائمة الانتظار مضمونة للوصول إلى الجذر - مُدمِّر افتراضي بحت. - إذا لم يقم المبرمج بإعادة تعريف مدمر افتراضي خالص في الفئة المشتقة ، فسوف يقوم المترجم بذلك نيابة عنه (انظر القسم 1.1). وبالتالي ، يمكن لفئة مشتقة من فئة مجردة مع مدمر افتراضي بحت أن تفقد تجريدها دون تجاوز المدمر بشكل صريح.
يمكن العثور على مثال لاستخدام مدمر افتراضي خالص في القسم 4.4.
3. فئات الواجهة
فئة الواجهة هي فئة مجردة لا تحتوي على بيانات وتتكون بشكل أساسي من وظائف افتراضية بحتة. يمكن أن يكون لهذه الفئة وظائف افتراضية عادية (ليست افتراضية بحتة) ، على سبيل المثال ، مدمر. قد تكون هناك أيضًا وظائف عضو ثابتة ، مثل وظائف المصنع.
3.1. تطبيقات
سيطلق على تطبيق فئة الواجهة اسم فئة مشتقة يتم فيها إعادة تعريف الوظائف الافتراضية البحتة. يمكن أن يكون هناك العديد من التطبيقات من نفس فئة الواجهة ، ومخططان ممكنان: أفقي ، عندما ترث عدة فئات مختلفة نفس فئة الواجهة ، ورأسيًا ، عندما تكون فئة الواجهة هي أصل التسلسل الهرمي المتعدد الأشكال. بالطبع ، قد يكون هناك هجينة.
النقطة الرئيسية لمفهوم فئات الواجهة هي الفصل الكامل للواجهة من التنفيذ - يعمل العميل فقط مع فئة الواجهة ، والتطبيق غير متاح لها.
3.2. إنشاء كائن
يسبب عدم إمكانية الوصول إلى فئة التنفيذ مشاكل معينة عند إنشاء الكائنات. يجب على العميل إنشاء مثيل لفئة التنفيذ والحصول على مؤشر لفئة الواجهة التي سيتم من خلالها الوصول إلى الكائن. نظرًا لأن فئة التنفيذ غير متاحة ، لا يمكنك استخدام المُنشئ ، وبالتالي ، يتم استخدام وظيفة المصنع ، والتي يتم تحديدها من جانب التنفيذ. عادة ما تقوم هذه الوظيفة بإنشاء كائن باستخدام عامل التشغيل new
وإرجاع مؤشر إلى الكائن الذي تم إنشاؤه ، ثم تحويله إلى مؤشر إلى فئة واجهة. يمكن أن تكون وظيفة المصنع عضوًا ثابتًا في فئة واجهة ، ولكنها ليست ضرورية ، على سبيل المثال ، يمكن أن تكون عضوًا في فئة مصنع خاصة (والتي بدورها يمكن أن تكون في حد ذاتها فئة واجهة) أو وظيفة مجانية. لا يمكن لوظيفة المصنع إعادة مؤشر خام إلى فئة واجهة ، بل مؤشر ذكي. تمت مناقشة هذا الخيار في القسمين 3.3.4 و 4.3.2.
3.3. حذف الكائن
إزالة كائن عملية حرجة للغاية. ينتج عن الخطأ إما تسرب للذاكرة أو حذف مزدوج ، مما يؤدي عادة إلى تعطل البرنامج. أدناه تعتبر هذه المشكلة مفصلة قدر الإمكان ، مع إيلاء الكثير من الاهتمام لمنع الإجراءات الخاطئة للعملاء.
هناك أربعة خيارات رئيسية:
- باستخدام عامل
delete
. - استخدام وظيفة افتراضية خاصة.
- استخدام وظيفة خارجية.
- الحذف التلقائي باستخدام المؤشر الذكي.
3.3.1. باستخدام عامل delete
للقيام بذلك ، يجب أن يكون لديك مدمر ظاهري مفتوح في فئة الواجهة. في هذه الحالة ، يقوم عامل delete
، الذي يسمى بمؤشر لفئة واجهة على جانب العميل ، بتقديم استدعاء إلى المدمر لفئة التنفيذ. قد يعمل هذا الخيار ، ولكن من الصعب التعرف عليه على أنه ناجح. نتلقى مكالمات المشغلين new
delete
المشغلين على جوانب مختلفة من "الحاجز" ، new
على جانب التنفيذ ، delete
على جانب العميل. وإذا تم تنفيذ فئة الواجهة في وحدة منفصلة (وهو أمر شائع تمامًا) ، فإننا نحصل على انتهاك للقاعدة من القسم 1.4.
3.3.2. استخدام وظيفة افتراضية خاصة
أكثر تقدمًا هو خيار آخر: يجب أن يكون لفئة الواجهة وظيفة افتراضية خاصة تزيل الكائن. هذه الوظيفة ، في النهاية ، تعود إلى استدعاء delete this
، لكن هذا يحدث بالفعل على جانب التنفيذ. يمكن استدعاء هذه الوظيفة بطرق مختلفة ، على سبيل المثال Delete()
، ولكن يتم استخدام خيارات أخرى أيضًا: Release()
و Destroy()
و Dispose()
و Free()
و Close()
وما إلى ذلك. بالإضافة إلى اتباع القاعدة في القسم 1.4 ، فإن هذا الخيار له العديد من المزايا الإضافية.
- يسمح لك باستخدام وظائف تخصيص / توزيع الذاكرة المعرفة من قبل المستخدم لفئة التنفيذ.
- يسمح لك بتنفيذ مخطط أكثر تعقيدًا للتحكم في عمر كائن التنفيذ ، على سبيل المثال ، باستخدام عداد مرجعي.
في هذا التجسيد ، قد يتم تجميع محاولة حذف كائن باستخدام عامل delete
وحتى تنفيذها ، ولكن هذا خطأ. لمنع ذلك في فئة الواجهة ، يكفي أن يكون لديك مدمر محمي فارغ أو افتراضي محض (انظر القسم 1.3). لاحظ أنه يمكن إخفاء استخدام عامل delete
تمامًا ، على سبيل المثال ، تستخدم المؤشرات الذكية القياسية عامل الحذف لحذف كائن بشكل افتراضي ويتم دفن الرمز المقابل بعمق في تنفيذها. يسمح لك المدمر المحمي باكتشاف كل هذه المحاولات في مرحلة التجميع.
3.3.3. استخدام وظيفة خارجية
قد يجذب هذا الخيار تناظرًا معينًا في الإجراءات لإنشاء كائن وحذفه ، ولكن في الواقع ليس له مزايا على الإصدار السابق ، ولكن هناك العديد من المشاكل الإضافية. هذا الخيار غير موصى به للاستخدام ولا يُنظر إليه في المستقبل.
3.3.4. الحذف التلقائي باستخدام المؤشر الذكي
في هذه الحالة ، لا تُرجع وظيفة المصنع مؤشرًا خامًا إلى فئة واجهة ، ولكن مؤشرًا ذكيًا مناظرًا. يتم إنشاء هذا المؤشر الذكي على جانب التنفيذ ويغلف كائن الحذف ، الذي يحذف كائن التنفيذ تلقائيًا عندما يخرج المؤشر الذكي (أو نسخته الأخيرة) عن النطاق على جانب العميل. في هذه الحالة ، قد لا تكون هناك حاجة إلى وظيفة افتراضية خاصة لحذف كائن التنفيذ ، ولكن لا تزال هناك حاجة إلى مدمر محمي ، فمن الضروري منع الاستخدام الخاطئ لعامل delete
. (صحيح ، يجب ملاحظة أن احتمالية حدوث مثل هذا الخطأ تقل بشكل ملحوظ.) هذا الخيار تمت مناقشته بمزيد من التفصيل في القسم 4.3.2.
3.4. خيارات أخرى لإدارة عمر مثيل لفئة التنفيذ
في بعض الحالات ، قد يتلقى العميل مؤشرًا إلى فئة الواجهة ، ولكن لا يمتلكه. إن إدارة عمر كائن التنفيذ تقع بالكامل على جانب التنفيذ. على سبيل المثال ، يمكن أن يكون الكائن كائنًا فرديًا ثابتًا (هذا الحل مثالي للمصانع). يتعلق مثال آخر بالتفاعل ثنائي الاتجاه ، انظر القسم 3.7. لا يجب على العميل حذف مثل هذا الكائن ، ولكن هناك حاجة إلى مدمر محمي لفئة الواجهة هذه ، فمن الضروري منع الاستخدام الخاطئ لعامل delete
.
3.5. نسخ دلالات
بالنسبة لفئة الواجهة ، لا يمكن إنشاء نسخة من كائن التنفيذ باستخدام مُنشئ النسخ ، لذلك إذا كان النسخ مطلوبًا ، فيجب أن يكون للفئة وظيفة افتراضية تقوم بإنشاء نسخة من كائن التنفيذ وترجع مؤشرًا إلى فئة الواجهة. غالبًا ما تسمى هذه الوظيفة مُنشئ افتراضي ، واسمها التقليدي هو Clone()
أو Duplicate()
.
لا يُحظر استخدام عامل تعيين النسخ ، ولكن لا يمكن اعتباره فكرة جيدة. يتم دائمًا إقران عامل تعيين النسخ ؛ ويجب أن يتم إقرانه مع مُنشئ النسخ. عامل التشغيل الذي تم إنشاؤه بواسطة المترجم الافتراضي لا معنى له ؛ لا يفعل أي شيء. من الممكن نظريًا إعلان عامل التعيين افتراضيًا بحتة مع إعادة تعريف لاحقة ، ولكن التعيين الافتراضي ليس ممارسة موصى بها ؛ يمكن العثور على التفاصيل في [Meyers1]. علاوة على ذلك ، تبدو المهمة غير طبيعية للغاية: عادةً ما يتم الوصول إلى كائنات فئة التنفيذ من خلال مؤشر إلى فئة الواجهة ، وبالتالي ستبدو المهمة كما يلي:
* = *;
يُحظر تشغيل عامل التخصيص بشكل أفضل ، وإذا لزم الأمر ، فإن هذه الدلالات لها في فئة الواجهة الوظيفة الافتراضية المقابلة.
هناك طريقتان لحظر التعيين.
- أعلن حذف عامل التعيين (
=delete
). إذا كانت فئات الواجهة تشكل تسلسلاً هرميًا ، فهذا يكفي للقيام به في الفئة الأساسية. عيب هذه الطريقة هو أنها تؤثر على فئة التطبيق ، كما ينطبق الحظر عليها. - قم بتعريف بيان المهمة المحمية بتعريف افتراضي (
=default
). لا يؤثر هذا على فئة التطبيق ، ولكن في حالة التسلسل الهرمي لفئات الواجهة ، يجب إجراء مثل هذا الإعلان في كل فئة.
3.6. منشئ فئة الواجهة
في كثير من الأحيان ، لا يتم تعريف مُنشئ فئة واجهة. في هذه الحالة ، يقوم المترجم بإنشاء المُنشئ الافتراضي المطلوب لتنفيذ الوراثة (انظر القسم 1.1). هذا المنشئ مفتوح ، على الرغم من أنه كافٍ ليكون آمنًا. إذا تم التصريح بحذف مُنشئ النسخ في فئة الواجهة ( =delete
) ، فسيتم منع الإنشاء بواسطة مترجم المُنشئ بشكل افتراضي ، ويجب الإعلان عن هذا المنشئ بشكل صريح. من الطبيعي جعلها آمنة مع تعريف افتراضي ( =default
). من حيث المبدأ ، يمكن الإعلان عن هذا المنشئ المحمي دائمًا. يوجد مثال في القسم 4.4.
3.7. تفاعل ثنائي الاتجاه
فصول الواجهة مناسبة لاستخدام الاتصال ثنائي الاتجاه. إذا كان يمكن الوصول إلى بعض الوحدات النمطية من خلال فئات الواجهة ، فيمكن للعميل أيضًا إنشاء تطبيقات لبعض فئات الواجهة وتمرير المؤشرات إليها في الوحدة النمطية. من خلال هذه المؤشرات ، يمكن للوحدة استقبال الخدمات من العميل وكذلك نقل البيانات أو الإخطارات إلى العميل.
3.8. مؤشرات ذكية
نظرًا لأن الوصول إلى كائنات فئة التنفيذ يتم عادةً من خلال المؤشر ، فمن الطبيعي استخدام المؤشرات الذكية للتحكم في عمرها. ولكن يجب أن يوضع في الاعتبار أنه إذا تم استخدام الخيار الثاني لحذف الكائنات ، فمع المؤشر الذكي القياسي ، من الضروري نقل deleter deleter (نوع) أو مثيل من هذا النوع. إذا لم يتم ذلك ، فسيستخدم المؤشر الذكي عامل الحذف لحذف الكائن ، ولن يتم تجميع الرمز ببساطة (بفضل المدمر المحمي). تتم مناقشة المؤشرات الذكية القياسية (بما في ذلك استخدام المزيلات المخصصة) بالتفصيل في [Josuttis] ، [Meyers3]. يمكن العثور على مثال لاستخدام مزيل مخصص في القسم 4.3.1.
, , , .
3.9. -
- const. , , -, .
3.10. COM-
COM- , , COM — , COM- , C, , . COM- C++ , COM.
3.11.
(API) (SDK). . -, -, . , (Windows DLL), : -. . , , . LoadLibrary()
, -, .
4.
4.1.
, .
class IBase { protected: virtual ~IBase() = default;
.
class IActivatable : public IBase { protected: ~IActivatable() = default;
, , . , IBase
. , (. 1.3). , .
4.2.
class Activator : private IActivatable {
, , , - , .
4.3.
4.3.1.
. - ( IBase
):
struct BaseDeleter { void operator()(IBase* p) const { p->Delete(); } };
std::unique_ptr<>
- :
template <class I> // I — IBase using UniquePtr = std::unique_ptr<I, BaseDeleter>;
, , - , UniquePtr
.
-:
template <class I> // I — - CreateInstance() UniquePtr<I> CreateInstance() { return UniquePtr<I>(I::CreateInstance()); }
:
template <class I> // I — IBase UniquePtr<I> ToPtr(I* p) { return UniquePtr<I>(p); }
std::shared_ptr<>
std::unique_ptr<>
, , std::shared_ptr<>
. Activator
.
auto un1 = CreateInstance<IActivatable>(); un1->Activate(true); auto un2 = ToPtr(IActivatable::CreateInstance()); un2->Activate(true); std::shared_ptr<IActivatable> sh = CreateInstance<IActivatable>(); sh->Activate(true);
( — -):
std::shared_ptr<IActivatable> sh2(IActivatable::CreateInstance());
std::make_shared<>()
, ( ).
: , . : , - . 4.4.
4.3.2.
. -. std::shared_ptr<>
, , ( ). std::shared_ptr<>
( ) - , delete
. std::shared_ptr<>
- ( ) - . .
#include <memory> class IActivatable; using ActPtr = std::shared_ptr<IActivatable>; // class IActivatable { protected: virtual ~IActivatable() = default; // IActivatable& operator=(const IActivatable&) = default; // public: virtual void Activate(bool activate) = 0; static ActPtr CreateInstance(); // - }; // class Activator : public IActivatable { // ... public: Activator(); // ~Activator(); // void Activate(bool activate) override; }; Activator::Activator() {/* ... */} Activator::~Activator() {/* ... */} void Activator::Activate(bool activate) {/* ... */} ActPtr IActivatable::CreateInstance() { return ActPtr(new Activator()); }
- std::make_shared<>()
:
ActPtr IActivatable::CreateInstance() { return std::make_shared<Activator>(); }
std::unique_ptr<>
, , - , .
4.4.
C# Java C++ «», . . IBase
.
class IBase { protected: IBase() = default; virtual ~IBase() = 0;
, Delete()
, .
IBase::~IBase() = default; void IBase::Delete() { delete this; }
IBase
. Delete()
, . - IBase
. Delete()
, - . Delete()
, . , 4.3.1.
5. ,
5.1
, , , , .
, , IException
Exception
.
class IException { friend class Exception; virtual IException* Clone() const = 0; virtual void Delete() = 0; protected: virtual ~IException() = default; public: virtual const char* What() const = 0; virtual int Code() const = 0; IException& operator=(const IException&) = delete; }; class Exception { IException* const m_Ptr; public: Exception(const char* what, int code); Exception(const Exception& src) : m_Ptr(src.m_Ptr->Clone()) {} ~Exception() { m_Ptr->Delete(); } const IException* Ptr() const { return m_Ptr; } };
Exception
, IException
. , throw
, . Exception
, . - , .
Exception
, , .
IException
:
class ExcImpl : IException { friend class Exception; const std::string m_What; const int m_Code; ExcImpl(const char* what, int code); ExcImpl(const ExcImpl&) = default; IException* Clone() const override; void Delete() override; protected: ~ExcImpl() = default; public: const char* What() const override; int Code() const override; }; ExcImpl::ExcImpl(const char* what, int code) : m_What(what), m_Code(code) {} IException* ExcImpl::Clone() const { return new ExcImpl(*this); } void ExcImpl::Delete() { delete this; } const char* ExcImpl::What() const { return m_What.c_str(); } int ExcImpl::Code() const { return m_Code; }
Exception
:
Exception::Exception(const char* what, int code) : m_Ptr(new ExcImpl(what, code)) {}
, — .NET — , — , C++/CLI. , , , C++/CLI.
5.2
- :
template <typename T> class ICollect { protected: virtual ~ICollect() = default; public: virtual ICollect<T>* Clone() const = 0; virtual void Delete() = 0; virtual bool IsEmpty() const = 0; virtual int GetCount() const = 0; virtual T& GetItem(int ind) = 0; virtual const T& GetItem(int ind) const = 0; ICollect<T>& operator=(const ICollect<T>&) = delete; };
, -, .
template <typename T> class ICollect; template <typename T> class Iterator; template <typename T> class Contain { typedef ICollect<T> CollType; CollType* m_Coll; public: typedef T value_type; Contain(CollType* coll); ~Contain();
. , . , , , , - begin()
end()
, . (. [Josuttis]), for
. . , , .
6. -
. -, . . , ++. , .NET, Java Pyton. . , , . .NET Framework C++/CLI C++. .
7.
-, .
.
delete
.- .
- .
.
, delete
. , .
- , . , , delete
.
.
, , , , .
[GoF]
Gamma E. ، Helm R. ، Johnson R. ، Vlissides J. طرق التصميم الكينوني. أنماط التصميم.: Per. من اللغة الإنجليزية - سانت بطرسبرغ: بيتر ، 2001.
[Josuttis]
Josattis ، Nikolai M. C ++ Standard Library: مرجع مرجعي ، الطبعة الثانية: Per. من اللغة الإنجليزية - م: LLC "I.D. ويليامز ، 2014.
[Dewhurst]
Dewhurst ، ستيفان K. أماكن زلق C ++. كيفية تجنب المشاكل عند تصميم وتجميع البرامج الخاصة بك. من اللغة الإنجليزية - م: مطبعة DMK ، 2012.
[Meyers1]
, . C++. 35 .: . . — .: , 2000.
[Meyers2]
, . C++. 55 .: . . — .: , 2014.
[Meyers3]
, . C++: 42 C++11 C++14.: . . — .: «.. », 2016.
[Sutter]
, . C++.: . . — : «.. », 2015.
[Sutter/Alexandrescu]
, . , . ++.: . . — .: «.. », 2015.