C++ / المتقدم / الدرس 16
تعدد الأشكال والتصاميم البديلة
اختر الواجهات التنفيذية أو القوالب أو البدائل المغلقة وفق حاجة التوسعة، مع حفظ الملكية والإتلاف ودلالات القيمة.
في هذه الصفحة
أهداف الدرس
- استخدام واجهة افتراضية بمدمّر افتراضي وoverride.
- تجنب تقطيع الكائنات والملكية غير الآمنة عبر مؤشرات الأساس.
- مقارنة الهرميات المفتوحة بـvariant وتعدد الأشكال الساكن.
اختيار جهة التوسعة المطلوبة
يتيح تعدد الأشكال تمثيل تطبيقات متعددة بواجهة واحدة. تناسب الهرمية التنفيذية مجموعة تطبيقات مفتوحة تُختار عبر مراجع الأساس أو مؤشراته. ويناسب القالب اختلافًا وقت الترجمة بعمليات يراها المترجم. تمثّل std::variant مجموعة بدائل مغلقة وتظهر المعالجة الشاملة. ابدأ بنوع التوسعة المتوقع: هل تأتي أنواع جديدة من وحدة أخرى، أم تضاف عمليات على مجموعة ثابتة؟ تجنب الوراثة لمشاركة حقول قليلة فقط؛ فكثيرًا ما يحفظ التركيب عقدًا أبسط ودلالات قيمة أوضح.
قصدية الاستدعاء والإتلاف الديناميكيين
تختار الدالة الافتراضية التنفيذ النهائي وفق النوع الديناميكي للكائن. تطلب override من المترجم التحقق أن التصريح يستبدل فعلًا دالة افتراضية في الأساس. يسمح مدمّر عام افتراضي بالحذف الآمن عبر مؤشر أساس مالك. يخزن المثال مستطيلًا مشتقًا في unique_ptr<Shape> ويستدعي area عبر الأساس ويتلفه بالمالك نفسه. الواجهة const للمراقبة. تبقي الأبعاد الصغيرة الضرب قابلًا للتمثيل؛ وتحدد واجهة هندسية فعلية الوحدات والحدود العددية ضمن عقدها.
#include <iostream>
#include <memory>
struct Shape {
virtual ~Shape() = default;
virtual int area() const = 0;
};
class Rectangle final : public Shape {
int width_;
int height_;
public:
Rectangle(int width, int height) : width_(width), height_(height) {}
int area() const override { return width_ * height_; }
};
int main() {
std::unique_ptr<Shape> shape = std::make_unique<Rectangle>(3, 4);
std::cout << shape->area() << "\n";
}
12
يصل الاستدعاء إلى Rectangle::area ويطبع اثني عشر. يجعل المدمّر الافتراضي الإتلاف عبر unique_ptr<Shape> صالحًا.
قيمة الأساس ليست قيمة المشتق كاملة
يحذف نسخ مشتق إلى كائن أساس بالقيمة الجزء المشتق، ويسمى ذلك تقطيعًا. لذلك لا تحفظ حاوية قيم الأساس سلوك المشتقات الاعتباطية. استخدم مراجع أو مؤشرات مالكة لمجموعة متعددة الأشكال، أو بديلًا بالقيمة مثل variant. يجب أن يحترم المشتق عقد الأساس؛ فتشديد شروطه السابقة أو إضعاف نتائجه الموعودة يخرق الاستبدال. لا تتصرف الاستدعاءات الافتراضية أثناء الإنشاء والإتلاف كأن الكائن الأكثر اشتقاقًا حي كاملًا، فلا تعتمد عليها لتهيئة المشتق أو تنظيفه.
معالجة مجموعة مغلقة بالزيارة
يخزن المثال الثاني RectangleValue أو SquareValue في variant. يعالج الزائر البديلين صراحة دون حاجة في التصميم إلى صنف أساس أو تخصيص ديناميكي. تستلزم إضافة بديل مراجعة الزائر، وقد يكون ذلك مرغوبًا لقواعد عمل شاملة. قد تصبح variant بلا قيمة بسبب استثناء أثناء بعض العمليات الرامية؛ فاحترم الاحتمال إذا كان إنشاء البدائل أو إسنادها قابلًا للفشل. البديلان هنا تجميعات صحيحة بسيطة، ولا يتضمن الإنشاء والزيارة للقراءة انتقالًا كهذا.
#include <iostream>
#include <variant>
struct RectangleValue { int width; int height; };
struct SquareValue { int side; };
struct Area {
int operator()(const RectangleValue& value) const { return value.width * value.height; }
int operator()(const SquareValue& value) const { return value.side * value.side; }
};
int main() {
const std::variant<RectangleValue, SquareValue> shape = SquareValue{5};
std::cout << std::visit(Area{}, shape) << "\n";
}
25
البديل النشط مربع ضلعه خمسة، فيعيد التحميل الموافق خمسة وعشرين.
فصل حدود المصدر عن التوافق الثنائي
لا تعني واجهة مصدر مستقرة توافقًا ثنائيًا تلقائيًا بين المترجمات وبناءات المكتبة أو بعد تغير تخطيط الأصناف. تحتاج أنظمة الإضافات إلى سياسة توافق وتخصيص صريحة. تجنب نقل الملكية عبر حد تتعارض فيه بيئتا التخصيص والتحرير. داخل التطبيق اختر أبسط تصميم يدعم التوسعة المطلوبة ثم قِس كلفة الاستدعاء الديناميكي في سياقه. قد تزيد القوالب بدل كل استدعاء افتراضي زمن الترجمة وحجم الشيفرة. وثّق الفشل وسلامة الخيوط وإمكان نسخ كائنات الواجهة أو نقلها أو الاحتفاظ بها.
الأخطاء الشائعة وإصلاحها
إتلاف مشتق عبر مالك أساس غير آمن.
- لماذا يحدث؟
- يفتقر الأساس العام القابل للحذف إلى مدمّر افتراضي.
- كيف تصلحه؟
- وفّر مدمّرًا عامًا افتراضيًا لعقد الملكية هذا.
- كيف تتجنب تكراره؟
- راجع الإتلاف عند إدخال ملكية متعددة الأشكال.
اختفاء سلوك المشتق في مجموعة قيم أساس.
- لماذا يحدث؟
- قطع النسخ بالقيمة الجزء المشتق.
- كيف تصلحه؟
- استخدم مالكين متعددين أو بديلًا صريحًا بالقيمة.
- كيف تتجنب تكراره؟
- افصل هرميات الهوية عن أنواع القيمة.
استبدال مقصود لا يُستدعى.
- لماذا يحدث؟
- يختلف توقيعه عن دالة الأساس الافتراضية.
- كيف تصلحه؟
- طابق الثبات والمعاملات وأضف override.
- كيف تتجنب تكراره؟
- اشترط override على تنفيذ الدوال الافتراضية المشتقة.
اختبر فهمك
1. يجب أن تقبل واجهة دفع تطبيقات مستقبلية من وحدات منفصلة. قارن واجهة افتراضية وvariant ثابتة لهذا المتطلب.
اعرض الحل المشروح
تتيح الواجهة الافتراضية مشتقات جديدة دون تعديل قائمة بدائل مركزية، فتلائم المجموعة المفتوحة. تتطلب variant سرد البدائل وتحديث الزوار فتلائم نموذجًا مغلقًا أكثر. حدّد الملكية والإتلاف الافتراضي والأخطاء وحدود التوافق صراحة؛ فالانفتاح لا يزيل هذه الالتزامات.
2. اجعل RectangleValue{2, 7} بديل variant النشط. توقّع النتيجة واشرح عدم الحاجة إلى تحويل هابط.
اعرض الحل المشروح
تختار std::visit العملية Area::operator()(const RectangleValue&) وتعيد أربعة عشر. تتتبع variant البديل النشط فيُختار التنفيذ وفق النوع دون تحويل مؤشر أساس. ينبغي كشف إضافة بديل ثالث بلا زائر متوافق أثناء الترجمة بدل اختيار فرع غير مرتبط بصمت.
أهم ما تعلمته
- اختر تعدد الأشكال التنفيذي المفتوح أو القيمي المغلق بوعي.
- احمِ الإتلاف متعدد الأشكال وتجنب التقطيع.
- احفظ عقود الأساس ووثّق توافق الحدود.