سلسلة ++c للمحترفين الدرس 21: الأصناف Classes والبنيات Structures في Cpp


محمد الميداوي

أساسيات الأصناف

الصنف (class) هو نوع يعرّفه المستخدم، ويُسبق بالكلمة المفتاحية ‎class‎ أو ‎struct‎ أو ‎union‎، ويشير المصطلح "class" بشكل عام إلى الأصناف غير الاتحاديّة (non-union classes).

والصنف مؤلّف من أعضاء يمكن أن تكون أيًا مما يلي:

  • متغيرات أعضاء، وتسمى كذلك متغيِّرات عضوية أو حقولًا (member variables)،
  • توابع، وتسمى كذلك دوالًا تابعة أو دوالًا أعضاءً (member functions)،
  • أنواع عضوية أو أنواع تعريفية (typedefs أو member types)
  • قوالب عضوية أو قوالب أعضاء (member templates) من أيّ نوع: متغير، دالّة، صنف أو قالب.

الكلمتان المفتاحيّتان ‎class‎ و ‎struct‎ واللّتان تُسمّيان مفاتيح الأصناف (class keys) متشابهتان إلى حدّ كبير، باستثناء أنّ محدد الوصول الافتراضي للأعضاء والأصناف الأساسية (bases) تكون خاصّة (private) في الأصناف التي صُرِّح عنها باستخدام المفتاح ‎class‎، وعامّة (public) بالنسبة للأصناف التي صُرِّح عنها باستخدام أحد المفتاحين ‎struct‎ أو ‎union‎. على سبيل المثال، المُقتطفان التاليان متطابقان:

struct Vector {
    int x;
    int y;
    int z;
};
// تكافئ
class Vector {
    public:
        int x;
    int y;
    int z;
};

بعد التصريح عن صنف، يُضاف نوع جديد إلى برنامجك، ومن الممكن استنساخ كائنات من هذا الصنف على النحو التالي:

Vector my_vector;

ويمكن الوصول إلى أعضاء الصّنف باستخدام الصياغة النقطيّة.

my_vector.x = 10;
my_vector.y = my_vector.x + 1; // my_vector.y = 11;
my_vector.z = my_vector.y - 4; // my:vector.z = 7;

الأصناف والبِنيات النهائية

الإصدار ≥ C++‎ 11

يُمكن حظر اشتقاق صنف بواسطة الكلمة المفتاحية ‎final‎، وفق الصياغة التالية:

class A final {
};

ستؤدّي أيُّ محاولة لاشتقاقه إلى خطأ تصريفي:

// Compilation error: cannot derive from final class
class B : public A {};

يمكن أن تظهر الأصناف النهائية في أيّ مكان في هرميّة الأصناف (class hierarchy):

class A {};
// OK.
class B final: public A {};
// Compilation error: cannot derive from final class B.
class C: public B {};

محددات الوصول (Access specifiers)

الكلمة المفتاحية الوصف
public الجميع لديهم صلاحية الوصول
protected الصنف والأصناف المشتقة منه وأصدقاء الصنف هم من لهم صلاحية الوصول
private الصنف وأصدقاء الصنف فقط لديهم حق الوصول

إذا عُرِّف النوع باستخدام الكلمة المفتاحية ‎class‎، سيكون مُحدِّد الوصول الافتراضي هو ‎private‎، ولكن إذا عُرِّف باستخدام ‎struct‎، فإنّ محدِّد الوصول الافتراضي الخاص به سيكون ‎public‎:

struct MyStruct {
    int x;
};
class MyClass {
    int x;
};
MyStruct s;
s.x = 9; // عام x خلل في الصياغة، لأنّ
MyClass c;
c.x = 9; // خاصّ x خلل في الصياغة، لأنّ

غالبًا ما تُستخدم مُحدِّدات الوصول لتقييد إمكانية الوصول إلى الحقول والتوابع الداخلية، إذ تجبر تلك المحدِّدات المُبرمج على استخدام واجهة برمجية محدّدة، فمثلًا لفرض استخدام الجالِبات (توابع الجلب - getters) والمُعيِّنات (توابع التعيين - setters) بدلاً من الرجوع مباشرة إلى المتغيرات:

class MyClass {
    public: /* Methods: */
        int x() const noexcept {
            return m_x;
        }
    void setX(int
        const x) noexcept {
        m_x = x;
    }
    private: /* Fields: */
        int m_x;
};

يُعدُّ استخدام الكلمة المفتاحيّة ‎protected‎ مفيدًا لقصر حق الوصول إلى بعض الوظائف على الأصناف المشتقّة، فمثلًا في الشيفرة التالية، يكون الوصول إلى التابع ‎calculateValue()‎ مقصورًا على الأصناف المشتقّة من الصنف ‎Plus2Base‎. انظر:

struct Plus2Base {
    int value() noexcept {
        return calculateValue() + 2;
    }
    protected: /* Methods: */
        virtual int calculateValue() noexcept = 0;
};
struct FortyTwo: Plus2Base {
    protected: /* Methods: */ int calculateValue() noexcept final override {
        return 40;
    }
};

لاحظ أنّه يمكن استخدام الكلمة المفتاحية ‎friend‎ لمنح تراخيص استثنائية إلى بعض الدوالّ أو الأنواع من أجل الوصول إلى الأعضاء المحمييّن والخواص. كذلك يمكن استخدام الكلمات المفتاحية ‎public‎ و ‎protected‎ و ‎private‎ لمنح أو تقييد حقّ الوصول إلى الكائنات الفرعية للصنف الأساسي.

الوراثة (Inheritance)

يمكن للأصناف والبُنى أن تكون بينها علاقات وراثة، فإذا ورث صنف أو بنية ‎B‎ من صنف أو بنية ‎A‎، فإنّ هذا يعني أنّ ‎B‎ هو أب لـ ‎A‎. ونقول أنّ ‎B‎ هو صنف أو بنية مشتقة من ‎A‎، وأنّ ‎A‎ هو الصنف أو البنية الأساسية (base class/struct)، أو نقول اختصارًا "أساس".

struct A {
    public: int p1;
    protected: int p2;
    private: int p3;
};
// A يرث من  B اجعل
struct B: A {};

هناك ثلاثة أشكال من الوراثة:

  • ‎public‎
  • ‎private‎
  • ‎protected‎

لاحظ أنّ الوراثة الافتراضية تماثل الرؤية الافتراضية للأعضاء: أي أنها تكون عامّة (‎public‎) في حال استخدام الكلمة المفتاحية ‎struct‎، أو خاصّة (‎private‎) في حال استخدام الكلمة المفتاحية ‎class‎.

من الممكن اشتقاق صنف من بِنية (أو العكس). ويُتحكَّم في الوراثة الافتراضية هنا بواسطة الصنف الابن/الفرعي (child)، وعليه فإنّ بنيةً مشتقة من صنف ستكون وراثتها الافتراضية عامّة (public)، وصنفٌ (‎class‎) مشتق من بنية (‎struct‎) ستكون وراثته الافتراضية خاصة (private).

الوراثة العامة (‎public‎):

struct B: public A // `struct B : A` أو
{
    void foo() {
        p1 = 0; // B عام في p1 
        p2 = 0; // B محمي  p2 
        p3 = 0; // B خاص في p3 صيغة غير صحيحة، لأن
    }
};
B b;
b.p1 = 1; // عام p1 
b.p2 = 1; // محمي p2 خطأ
b.p3 = 1; // غير قابل للوصول p2 خطأ لأن

الوراثة الخاصة (‎private‎):

struct B: private A {
    void foo() {
        p1 = 0; // B خاص في  p1 
        p2 = 0; // B خاص في p2 
        p3 = 0; // A خاص في p3 خطأ،لأنّ
    }
};
B b;
b.p1 = 1; // خاص p3 خطأ،لأنّ
b.p2 = 1; // خاص p2 خطأ،لأنّ
b.p3 = 1; // غير قابل للوصول p3 خطأ، لأنّ

الوراثة المحميّة (‎protected‎):

struct B: protected A {
    void foo() {
        p1 = 0; // B محمي في  p1 
        p2 = 0; // B محمي في  p2 
        p3 = 0; // A خاص في p2 خطأ، لأنّ 
    }
};
B b;
b.p1 = 1; // محمي  p1 خطأ، لأن
b.p2 = 1; // محمي  p2 خطأ لأنّ
b.p3 = 1; // غير قابل للوصول p3 خطأ لأنّ

لاحظ أنه على الرغم من أنّ الوراثة المحميّة ‎protected‎ مسموح بها إلا أنّ استخدامها ناد، فمن أمثلة استخدامها في التخصيص الجزئي لصنف أساسي (يشار إليه عادةً باسم "التعددية الشكلية المحكومة".

كان يُنظَر في بدايات البرمجة الكائنية (OOP) إلى الوراثة (العامة) كعلاقة انتماء ("IS-A")، وهذا يعني أنّ الوراثة العامة لا تكون صحيحة إلا إذا كانت نُسخ الصنف المشتق أيضًا نُسخًا من الصنف الأساسي، وقد استُبدِلَ بهذا المبدأ مبدأ آخر، وهو مبدأ Liskov للتعويض:

اقتباس

يجب ألا تُستخدم الوراثة العامة إلا عندما يكون من الممكن استبدال نُسخة من الصنف المشتق بنُسخة من الصنف الأساسي في جميع الظروف (ويبقى ذلك منطقيًّا).

يقال عادة إنّ الوراثة الخاصة (Private inheritance) تجسّد علاقة مختلفة تمامًا، إذ يُعبَّر عنها عادة بالصيغة "منفَّذة وفق" (تُدعى أحيانًا علاقة "HAS-A"). على سبيل المثال، يمكن أن يرث صنف ‎Stack‎ بشكل خاص (privately) من صنف ‎Vector‎، وتشبه الوراثة الخاصة علاقة التجميع (aggregation) أكثر من شبهها بعلاقة الوراثة العامة.

ولا تكادُ تُستخدم الوراثة المحمية (Protected inheritance) على الإطلاق، ولا يوجد اتفاق عام على نوع العلاقة التي تجسّدها.

الصداقة (Friendship)

تُستخدم الكلمة المفتاحية ‎friend‎ لإعطاء الأصناف والدوالّ الأخرى حق الوصول إلى أعضاء الصنف الخواص والمحميّين، حتى لو كانت مُعرّفة خارج نطاق الصنف.

class Animal {
    private:
        double weight;
    double height;
    public:
        friend void printWeight(Animal animal);
    friend class AnimalPrinter;
    //  << من الاستخدامات الشائعة للدوالّ الصديقة هو زيادة تحميل المعامل 
    friend std::ostream & operator << (std::ostream & os, Animal animal);
};
void printWeight(Animal animal) {
    std::cout << animal.weight << "\n";
}
class AnimalPrinter {
    public:
        void print(const Animal & animal) {
            // يُسمح بالدخول إلى الأعضاء الخاصة الآن
            std::cout << animal.weight << ", " << animal.height << std::endl;
        }
}
std::ostream& operator<<(std::ostream& os, Animal animal) {
    os << "Animal height: " << animal.height << "\n";
    return os;
}
int main() {
    Animal animal = {
        10,
        5
    };
    printWeight(animal);
    AnimalPrinter aPrinter;
    aPrinter.print(animal);
    std::cout << animal;
}

الناتج:

10
10, 5
Animal height: 5

الوراثة الوهمية (Virtual Inheritance)

يمكنك استخدام الكلمة المفتاحية ‎virtual‎ وفق الصياغة التالية:

struct A{};
struct B: public virtual A{};

إذا كان لصنف ‎B‎ صنفٌ أساسيّ وهمي هو ‎A‎، فهذا يعني أنّ ‎A‎ سوف يكون في أكثر صنف مشتق، وعليه فإنّ الصنف الأكثر اشتقاقًا (most derived class) سيكون مسؤولًا عن تهيئة ذلك الصنف الأساسي الوهمي (virtual base):

struct A {
    int member;
    A(int param) {
        member = param;
    }
};

struct B: virtual A {
    B(): A(5) {}
};

struct C: B {
    C(): /*A(88)*/ {}
};

void f() {
    C object; // `A` لم يهيّئ الصنف الأساسي الوهمي C خطأ، لأنّ
}

إذا ألغينا تعليق /* A (88)‎ */، فلن يحدث خطأ، لأنّ ‎C‎ سيكون قد هيّأ أساسه الوهمي (أو صنفه الأب) ‎A‎. كذلك، لاحظ أنه عند إنشاء ‎object‎، فإنّ الصنف الأكثر اشتقاقا (most derived class) هو ‎C‎، وعليه فإنّ ‎C‎ هو المسؤول عن إنشاء (استدعاء مُنشئ) ‎A‎، ومن ثم فإنّ قيمة ‎A::member‎ ستكون ‎88‎، وليس ‎5‎ (كما سيكون الأمر لو أنشأنا كائنًا من النوع ‎B‎).

هذا مفيد لحل مشكلة الماسّة:

   A                                        A      A
 /     \                                       |      |
B    C                                      B   C
 \    /                                         \    /
   D                                            D
الوراثة الوهمية        vs               الوراثة العادية

يرث كلٌّ من ‎B‎ و ‎C‎ من ‎A‎، ويرث ‎D‎ من ‎B‎ و ‎C‎، لذا فهناك نُسختان من A في D! قد ينتج عن هذا بعض الغموض عند محاولة الوصول إلى عضو من ‎A‎ عبر ‎D‎، ذلك أنّه ليس للمُصرّف أي طريقة لتَخمين الصنف الذي تريد الوصول من خلاله إلى العضو (أهو الصنف الذي يرثه ‎B‎، أو الصنف الذي ورثه ‎C‎؟) .

تحل الوراثة الوهميّة (Virtual inheritance) هذه المشكلة على النحو التالي: نظرًا لأنّ الأصناف الأساسية الوهميّة تقبع في الكائنات الأكثر اشتقاقًا (most derived object)، فستكون هناك نُسخة واحدة فقط من ‎A‎ في ‎D‎.

struct A {
    void foo() {}
};

struct B: public /*virtual*/ A {};
struct C: public /*virtual*/ A {};

struct D: public B, public C {
    void bar() {
        foo();     // نستدعي؟ foo خطأ، فأي 
// B::foo() أم C::foo() هل 
    }
};

إزالة التعليقات سيُجلِّي هذا الغموض.

الوراثة الخاصة: تقييد واجهة الصنف الأساسي

الوراثة الخاصة مفيدة لتقييد الواجهة العامة للصنف:

class A {
    public:
        int move();
    int turn();
};

class B: private A {
    public: using A::turn;
};

B b;
b.move(); // خطأ في التصريف
b.turn(); // OK

يمنع هذا المنظور الوصول إلى التوابع العامة لـ A عبر مؤشّرٍ أو مرجع يشير إلى A:

B b;
A& a = static_cast<A&>(b); // خطأ في التصريف

في حالة الوراثة العامة، سيوفّر مثل هذا التحويل إمكانية الوصول إلى جميع التوابع العامة للصنف A، رغم وجود طرق بديلة لمنع ذلك من داخل الصنف المشتق B، مثل الإخفاء:

class B: public A {
    private:
int move();
};

أو استخدام الكلمة المفتاحية private:

class B: public A {
    private: 
using A::move;
};

وفي كلا الحالتين، سيكون من الممكن القيام بما يلي:

B b;
A& a = static_cast<A&>(b);    // جائز في الوراثة العامة
a.move();                // OK

الوصول إلى أعضاء الصنف

للوصول إلى متغيرات الأعضاء أو الدوالّ العضوية لكائن من صنف ما، فإننا نستخدم العامل ‎.‎:

struct SomeStruct {
    int a;
    int b;
    void foo() {}
};

SomeStruct
var;
// var في a الدخول إلى الحقل
std::cout <<
    var.a << std::endl;
// var في b تعيين الحقل
var.b = 1;
// استدعاء تابع
var.foo();

يُستخدم العامل ‎->‎ عادة عند محاولة الوصول إلى أعضاء الصنف عبر مؤشّر. كخيار بديل، يمكن تحصيل النُسخة واستخدام العامل ‎.‎، لكنّ هذا أقل شيوعًا:

struct SomeStruct {
    int a;
    int b;
    void foo() {}
};
SomeStruct var;
SomeStruct *p = &var;
// عبر مؤشّر a الوصول إلى المتغير العضو
std::cout << p -> a << std::endl;
std::cout << (*p).a << std::endl;
// عبر مؤشّر b تعيين متغير العضو
p -> b = 1;
(*p).b = 1;
// استدعاء دالة تابعة عبر مؤشّر
p -> foo();
(*p).foo();

للوصول إلى أعضاء الصنف الساكنة (static class members)، فإنّنا نستخدم العامل ‎::‎، ولكن مع اسم الصنف وليس نسخته، وكخيار بديل، يمكن الوصول إلى الأعضاء الساكنة من نُسخة أو مُؤشّر--إلى-نُسخة باستخدام العامل ‎.‎ أو ‎->‎ على الترتيب، وبنفس الصياغة المُستخدمة للوصول إلى الأعضاء غير الساكنة.

struct SomeStruct {
    int a;
    int b;
    void foo() {}
    static int c;
    static void bar() {}
};
int SomeStruct::c;
SomeStruct var;
SomeStruct* p = &var;
// SomeStruct في البنية c تعيين الحقل الساكن
SomeStruct::c = 5;
// p و var عبر SomeStruct في البنية c تعيين متغير العضو الساكن
var.a = var.c;
var.b = p->c;
// استدعاء دالة تابعة ساكنة
SomeStruct::bar();
var.bar();
p->bar();

تفصيل العامل ‎->‎ ضروريّ لأنّ عامل الوصول العضوي ‎.‎ له الأسبقية على عامل التحصيل ‎*‎، وقد يتوقع المرء أنّ ‎*p.a‎ سيحصل‏‏ ‎p‎ (لينتج عنه مرجع إلى الكائن المشار إليه من قِبل ‎p‎) ثم يصل إلى العضو ‎a‎. ولكن في الواقع، فإنّه يحاول الوصول إلى العضو ‎a‎ من ‎p‎ قبل أن يحصله، أي أن ‎*p.a‎ تكافئ ‎*(p.a)‎.

في المثال أعلاه، قد ينتج عن هذا خطأ في التصريف لسببين: أولاً، ‎p‎ مؤشّر وليس له عضو ‎a‎. ثانياً، ‎a‎ عدد صحيح، وبالتالي لا يمكن تحصيله.

أحد الحلول الممكنة -رغم عدم شيوعه- لهذه المشكلة هو التحكّم بشكل صريح (explicitly) في الأسبقية عبر الأقواس: ‎(*p).a‎. وهناك حل أكثر شيوعًا، وهو استخدام العامل ‎->‎، وهو حل مختصر لتحصيل المؤشّر ثم الوصول إليه. بمعنى آخر ‎(*p).a‎ تكافئ ‎p->a‎.

العامل ‎::‎ هو عامل النطاق (scope operator)، ويُستخدم بنفس طريقة الوصول إلى عضو في فضاء الاسم (namespace). ذلك أنّ الأعضاء الساكنة في الصنف تعدٌّ في نطاق ذلك الصنف، لكنها لا تُعدُّ أعضاءً في نُسخ ذلك الصنف. يُسمح أيضًا باستخدام ‎.‎ و ‎->‎ المعتادتان مع الأعضاء الساكنة، على الرغم من أنهما ليستا عضوتين في النُسخ لأسباب تاريخية، وهذا مفيد لكتابةِ الشيفرات العامة في القوالب لأنّ المُستدعِي لا يعنيه إن كان التابع ساكنًا أو غير ساكن.

أنواع الأعضاء، والأسماء البديلة / الكُنى

يمكن لصنف ‎class‎ أو بنية ‎struct‎ تعريف الكُنى/الأسماء البديلة (aliases) لنوع عضوي (member type)، وهي أسماء بديلة للنّوع توجد داخل الصنف نفسه وتُعامل كأعضاء منه.

struct IHaveATypedef {
    typedef int MyTypedef;
};

struct IHaveATemplateTypedef {
    template < typename T >
        using MyTemplateTypedef = std::vector < T > ;
};

يمكن الوصول إلى هذه التعريفات النوعية (typedefs)، مثل الأعضاء الساكنة، باستخدام عامل النطاق ‎::‎:

IHaveATypedef::MyTypedef i = 5; // عدد صحيح i 
IHaveATemplateTypedef::MyTemplateTypedef<int> v; // std::vector<int> من النوع v 

يُسمح لكُنى الأنواع العضوية -كما هو الحال مع أسماء النوع البديلة (type aliases) العادية- أن تشير إلى أيّ نوع سبق تعريفه أو تكنِيته (aliased) من قبل، لكن ليس بعد تعريفه. وبالمثل، يمكن أن يشير التعريف النوعي (typedef) الموجود خارج الصنف إلى أيّ تعريف نوعي يمكن الوصول إليه داخل الصنف، بشرط أن يأتي بعد تعريف الصنف.

struct IHaveATypedef {
    typedef int MyTypedef;
};
struct IHaveATemplateTypedef {
    template < typename T >
        using MyTemplateTypedef = std::vector < T > ;
};
template < typename T >
    struct Helper {
        T get() const {
            return static_cast < T > (42);
        }
    };
struct IHaveTypedefs {
    //    typedef MyTypedef NonLinearTypedef; // سيحدث خطأ في حال إلغاء التعليق
    typedef int MyTypedef;
    typedef Helper < MyTypedef > MyTypedefHelper;
};
IHaveTypedefs::MyTypedef i;        // x_i is an int.
IHaveTypedefs::MyTypedefHelper hi;    // x_hi is a Helper<int>.
typedef IHaveTypedefs::MyTypedef TypedefBeFree;
TypedefBeFree ii;                // ii is an int.

يمكن التصريح عن أسماء النوع العضوي البديلة عبر مستوى وصول (access level)، وستحترم معدَّل الوصول المناسب:

class TypedefAccessLevels {
    typedef int PrvInt;
    protected:
        typedef int ProInt;
    public:
        typedef int PubInt;
};

TypedefAccessLevels::PrvInt prv_i; // خاص TypedefAccessLevels::PrvInt خطأ، لأن
TypedefAccessLevels::ProInt pro_i; // محمي TypedefAccessLevels::ProInt خطأ، لأن
TypedefAccessLevels::PubInt pub_i; // حسنا
class Derived: public TypedefAccessLevels {
    PrvInt prv_i; // خاص TypedefAccessLevels::PrvInt خطأ، لأنّ
    ProInt pro_i; // حسنا
    PubInt pub_i; // حسنا
};

يساعد هذا على توفير قدر من التجريد، مما يسمح لمصمّم الصنف بتغيير عمله الداخلي دون تعطيل الشّيفرات التي تعتمد عليه.

class Something {
    friend class SomeComplexType;

   short s;
    // ...

    public:
        typedef SomeComplexType MyHelper;
    MyHelper get_helper() const {
        return MyHelper(8, s, 19.5, "shoe", false);
    }
    // ...
};
// ...

Something s;
Something::MyHelper hlp = s.get_helper();

في هذه الحالة، إذا تغير الصنف المساعد من ‎SomeComplexType‎ إلى نوع آخر فلن تحتاج إلّا إلى تعديل تصريح ‎typedef‎ و ‎friend‎ طالما يوفّر الصنف المساعد نفس الوظيفة، كما أنّ أيّ شيفرة تستخدمه كـ ‎Something::MyHelper‎ بدلاً من تحديده باسمه لن تحتاج إلى أيّ تعديلات. وهكذا نقلّل مقدار الشيفرة التي يجب تعديلها عند تغييرالتنفيذ إذ لن نحتاج إلى تغيير اسم النوع إلا في مكان واحد.

يمكنك أيضًا دمج هذا مع ‎decltype‎، إذا رغبت في ذلك.

class SomethingElse {
    AnotherComplexType < bool, int, SomeThirdClass > helper;
    public:
        typedef decltype(helper) MyHelper;
    private:
        InternalVariable < MyHelper > ivh;
    // ...
    public:
        MyHelper& get_helper() const { return helper; }
    // ...
};

في هذه الحالة، سيؤدي تغيير تقديم ‎SomethingElse::helper‎ إلى تغيير التعريف النوعي (typedef) تلقائيًا بفضل ‎decltype‎. هذا يقلّل من عدد التعديلات اللازمة عندما نريد تغيير الصنف المساعد ‎helper‎، كما يقلّل من الأخطاء البشرية.

إذا لم يكن اسم النوع يُستخدَم إلّا مرّة واحدة أو مرتين داخليًا، ولم يكن يُستخدَم أبدًا في الخارج على سبيل المثال، فليست هناك حاجة لتوفير كُنية له. أمّا إذا كان يُستخدم مئات أو آلاف المرّات داخل المشروع أو كان له اسم طويل، فقد يكون من المفيد توفيره كتعريف نوعي (typedef) بدلًا من استخدامه باسمه الأساسي. يجب عليك أن تدرس الأمر قبل اختيار الطريقة التي تريد العمل بها.

يمكن أيضًا استخدام هذا مع أصناف القوالب لتوفير حقّ الوصول إلى معاملات القالب من خارج الصنف.

template < typename T >
    class SomeClass {
        // ...

       public:
            typedef T MyParam;
         MyParam getParam() {
            return static_cast < T > (42);
        }
    };

template < typename T >
    typename T::MyParam some_func(T & t) {
        return t.getParam();
    }

SomeClass < int > si;
int i = some_func(si);

يَشيع استخدام هذا مع الحاويات التي عادة ما توفر نوع العنصر الخاص بها (والأنواع المساعِدة الأخرى) كأسماء بديلة لنوع عضوي (member type aliases). وتوفّر معظم الحاويات الموجودة في المكتبة القياسية للغة C++‎ الأنواع المساعدة الاثنتي عشر التالية، إضافة إلى أنواع خاصة أخرى.

template < typename T >
class SomeContainer {
        // ...
    public:
// توفير نفس الأنواع المساعدة للحاويات القياسية
typedef T                         value_type;
typedef std::allocator<value_type>         allocator_type;
typedef value_type&                 reference;
typedef const value_type&             const_reference;
typedef value_type*                 pointer;
typedef const value_type*                 const_pointer;
typedef MyIterator<value_type>             iterator;
typedef MyConstIterator<value_type>         const_iterator;
typedef std::reverse_iterator<iterator>         reverse_iterator;
typedef std::reverse_iterator<const_iterator>     const_reverse_iterator;
typedef size_t                     size_type;
typedef ptrdiff_t                     difference_type;
};

كان توفير "قالب ‎typedef‎" شائعًا قبل الإصدار C++‎ 11، وقد أصبحت هذه الطريقة أقل شيوعًا مع إضافة ميزة كُنَى القوالب، لكنّها ما تزال مفيدة في بعض المواقف، وتُدمج مع كُنى القوالب في مواقف أخرى، قد يكون هذا مفيدًا جدًا في الحصول على مكوِّنات فردية من نوع معقّد، مثل مؤشّر دالّة ما. كذلك يكثر استخدام الاسم ‎type‎ للاسم البديل للنوع. انظر:

template < typename T >
    struct TemplateTypedef {
        typedef T type;
    }
TemplateTypedef < int > ::type i;        // هو رقم صحيح i.

يُستخدم هذا غالبًا مع الأنواع التي لها عدّة معاملات قوالب، لتوفير اسم بديل يُعرّف معاملًا واحدًا أو أكثر.

template < typename T, size_t SZ, size_t D >
    class Array {
        /* ... */ };
template < typename T, size_t SZ >
    struct OneDArray {
        typedef Array < T, SZ, 1 > type;
    };
template < typename T, size_t SZ >
    struct TwoDArray {
        typedef Array < T, SZ, 2 > type;
    };
template < typename T >
    struct MonoDisplayLine {
        typedef Array < T, 80, 1 > type;
    };
OneDArray < int, 3 > ::type arr1i;        // Array<int, 3, 1> مصفوفة من النوع arr1i 
TwoDArray < short, 5 > ::type arr2s;    // Array<short, 5, 2> مصفوفة من النوع arr2s 
MonoDisplayLine < char > ::type arr3c;    // Array<char, 80, 1> مصفوفة من النوع arr3c

الأصناف والبِنيات المُتشعِّبة (Nested Classes/Structures)

يمكن أن يحتوي صنف ‎class‎ أو بنية ‎struct‎ على تعريف صنف أو بنية أخرى داخله، وسيُسمى "صنفًا متشعبًا" (nested class)، بينما يُشار إلى الصنف الذي يحتوي التعريف "بالصنف المُحيط" (enclosing class)، ويعدُّ الصنف المتشعِّب عضوًا في الصنف المُحيط لكنه منفصل عنه.

struct Outer {
    struct Inner {};
};

يمكن الوصول إلى الأصناف المتشعِّبة من خارج الصنف المُحيط باستخدام عامل النطاق، أمّا من داخل الصنف المُحيط، فيمكن استخدام الأصناف المتشعِّبة بدون مؤهلات:

struct Outer {
    struct Inner {};
    Inner in ;
};
// ...
Outer o;
Outer::Inner i = o.in;

وكما هو الحال مع الأصناف والبنيات غير المُتشعِّبة، يمكن تعريف التوابع والمتغيّرات الساكنة إما داخل الصنف المتشعِّب، أو في فضاء الاسم (namespace) المُحيط، لكن لا يمكن تعريفها داخل الصنف المُحيط لأنه يُعتبر صنفًا مختلفًا عن الصنف المتشعِّب.

// خطأ
struct Outer {
    struct Inner {
        void do_something();
    };

    void Inner::do_something() {}
};

// ممتاز
struct Outer {
    struct Inner {
        void do_something();
    };
};

void Outer::Inner::do_something() {}

بالمثل، وكما هو الحال مع الأصناف غير المتشعِّبة، يمكن التصريح مُسبقًا (forward declared) عن الأصناف المتشعِّبة ثم تعريفها لاحقًا، شريطة أن تُعرّف قبل استخدامها.

class Outer {
    class Inner1;
    class Inner2;

    class Inner1 {};

    Inner1 in1;
    Inner2* in2p;

    public:
        Outer();
    ~Outer();
};

class Outer::Inner2 {};

Outer::Outer(): in1(Inner1()), in2p(new Inner2) {}
Outer::~Outer() {
    if (in2p) {
        delete in2p;
    }
}

الإصدار <C++‎ 11

لم يكن للأصناف المتشعِّبة قبل الإصدار C++‎ 11 حق الوصول إلّا إلى أسماء الأنواع (type names) والأعضاء الساكنة (‎static‎) والعدّادات (enumerators) من الصنف المُحيط، ولم تكن الأعضاء الأخرى المعرَّفة في الصنف المحيط متاحة.

الإصدار ≥ C++‎ 11

وبدءًا من الإصدار C++‎ 11، تُعامل الأصناف المتشعِّبة وأعضاؤها كما لو كانت صديقة (‎friend‎) للصنف المُحيط، وصار بإمكانها الوصول إلى جميع أعضائه وفقًا لقواعد الوصول المعتادة، وإذا كان أعضاء الصنف المتشعِّب يحتاجون إلى تقييم عضو ساكن أو أكثر من الصنف المُحيط، فيجب أن تُمرّر نُسخة إليها:

class Outer {
    struct Inner {
        int get_sizeof_x() {
            return sizeof(x); // غير مُقيّم، لذلك هناك حاجة إلى نسخة x 
        }
        int get_x() {
            return x; // لا يمكن الوصول إلى الأعضاء غير الساكنين بدون نسخة
        }
        int get_x(Outer& o) {
            return o.x; // الوصول إلى الأعضاء الخاصة Inner بإمكان  Outer كعضو من 
        }
    };
    int x;
};

بالمقابل، لا يُعامَل الصنف المُحيط كصديق للصنف المتشعِّب، وعليه لا يمكنه الوصول إلى أعضائه دون الحصول على إذن صريح.

class Outer {
    class Inner {
        // friend class Outer;
        int x;
    };
    Inner in ;
    public:
        int get_x() {
            return in.x; // Error: int Outer::Inner::x is private.
            // لإصلاح المشكلة "friend" الغ تعليق
        }
};

لا يُعدّ أصدقاء الصنف المتشعِّب تلقائيًا أصدقاءً للصنف المحيط؛ فالصداقة مع الصنف المحيط ينبغي التصريح عنها بشكل منفصل. بالمقابل، بما أن الصنف المُحيط لا يُعدُّ صديقًا للصنف المتشعِّب تلقائيًا ، فلن يكون أصدقاء الصنف المُحيط أصدقاءً للصنف المتشعِّب.

class Outer {
   friend void barge_out(Outer& out, Inner& in);
    class Inner {
        friend void barge_in(Outer& out, Inner& in);
        int i;
    };
    int o;
};
void barge_in(Outer & out, Outer::Inner & in ) {
    int i = in .i; // جيد
    int o = out.o; // خاص int Outer::o خطأ: لأن
}
void barge_in(Outer& out, Outer::Inner& in) {
    int i = in .i; // خاص int Outer::Inner::i خطأ: لأن
    int o = out.o; // جيد
}

كما هو الحال مع جميع أعضاء الصنف الآخرين، لا يمكن تسمية الأصناف المتشعِّبة من خارج الصنف إلا إذا كانت عامّة (public). لكن يُسمح لك بالوصول إليها بغض النظر عن مُعدِّل الوصول طالما أنك لا تُسمّيها صراحة.

class Outer {
    struct Inner {
        void func() {
            std::cout << "I have no private taboo.\n";
        }
    };
    public:
        static Inner make_Inner() {
            return Inner();
        }
};
// ...
Outer::Inner oi; // خاص Outer::Inner خطأ: لأن
auto oi = Outer::make_Inner(); // جيد
oi.func(); // جيد
Outer::make_Inner().func(); // جيد

يمكنك أيضًا إنشاء كُنية نوع للصنف المتشعِّب، وإذا كانت كُنية النوع موجودة في الصنف المحيط فيمكن أن يكون للنوع المتشعِّب وكُنية النوع مُحدِّدا وصول مختلفّين. وإذا كانت كُنية النوع موجودة خارج الصنف المُحيط، فإن ذلك يتطلب إمّا أن يكون الصنف المتشعِّب عامًّا (public)، أو نوع التعريف (‎typedef‎)، أيهما.

class Outer {
    class Inner_ {};
    public:
        typedef Inner_ Inner;
};
typedef Outer::Inner ImOut; // جيد
typedef Outer::Inner_ ImBad; // خطأ
// ...
Outer::Inner oi; // جيد
Outer::Inner_ oi; // خطأ
ImOut oi; // جيد

كما هو الحال مع الأصناف الأخرى، يمكن الاشتقاق من الأصناف المتشعِّبة، ويمكنها أن تشتق من أصناف أخرى.

struct Base {};
struct Outer {
    struct Inner: Base {};
};
struct Derived: Outer::Inner {};

هذا مفيد في الحالات التي يكون هناك صنف مشتقّ من الصنف المُحيط، إذ يسمح ذلك للمُبرمج بتحديث الصنف المتشعِّب عند الضرورة، ويمكن دمج ذلك مع تعريف نوعيٍّ (typedef) لتوفير اسم ثابت لكل صنف متشعِّب من الصنف المُحيط:

class BaseOuter {
    struct BaseInner_ {
        virtual void do_something() {}
        virtual void do_something_else();
    }
    b_in;
    public:
    typedef BaseInner_ Inner;
    virtual ~BaseOuter() = default;
    virtual Inner & getInner() {
        return b_in;
    }
};
void BaseOuter::BaseInner_::do_something_else() {}
// ---
class DerivedOuter: public BaseOuter {
    // BaseOuter::BaseInner_ is private لاحظ استخدام النوع التعريفي المؤهل
    struct DerivedInner_: BaseOuter::Inner {
        void do_something() override {}
        void do_something_else() override;
    }
    d_in;
    public:
        typedef DerivedInner_ Inner;
    BaseOuter::Inner & getInner() override {
        return d_in;
    }
};
void DerivedOuter::DerivedInner_::do_something_else() {}
// ...
// BaseOuter::BaseInner_::do_something(); استدعاء
BaseOuter * b = new BaseOuter;
BaseOuter::Inner & bin = b -> getInner();
bin.do_something();
b -> getInner().do_something();
// DerivedOuter::DerivedInner_::do_something(); استدعاء
BaseOuter * d = new DerivedOuter;
BaseOuter::Inner & din = d -> getInner();
din.do_something();
d -> getInner().do_something();

في الحالة أعلاه، يقدّم كل من الصِّنفين ‎BaseOuter‎ و ‎DerivedOuter‎ النوع العُضوي ‎Inner‎ كـ ‎BaseInner_‎ و ‎DerivedInner_‎ على الترتيب، ويسمح هذا بااشتقاق الأنواع المتشعِّبة دون تعطيل واجهة الصِّنف المُحيط، كما يسمح باستخدام النوع المتشعِّب بأشكال متعددة.

البِنيات والأصناف غير المُسمّاة (Unnamed struct/class)

يُسمح باستخدام البِنيات غير المُسمّاة (نوع ليس له اسم):

void foo() {
    struct /* No name */ {
        float x;
        float y;
    }
    point;

    point.x = 42;
}

أو

struct Circle {
    struct /* بلا اسم */ {
        float x;
        float y;
    }
    center; // لكن مع أعضاء لها اسم
    float radius;
};

يمكن أن نكتب الآن:

Circle circle;
circle.center.x = 42.f;

لكن لا يُسمح بالبنيات المجهولة - anonymous struct - (نوع غير مسمّى وكائن غير مسمّى)

struct InvalidCircle {
    struct /* بلا اسم */ {
        float centerX;
        float centerY;
    }; // لا أعضاء كذلك
    float radius;
};

ملاحظة: تسمح بعض المُصرِّفات بالبُنى المجهولة كملحقات.

الإصدار ≥ C++‎ 11

  • يمكن النظر إلى تعبيرات لامدا كبنية خاصة غير مُسمّاة.
  • تسمح ‎decltype‎ بالحصول على نوع بنية غير مسمّاة:
decltype(circle.point) otherPoint;
  • يمكن أن تكون نُسخ البنيات غير المسمّاة معاملات لتابع قالب (template method):
void print_square_coordinates()
{
const struct {float x; float y;} points[] = {
{-1, -1}, {-1, 1}, {1, -1}, {1, 1}
};

// template <class T, std::size_t N> std::begin(T (&)[N]) بالنسبة لمجال يعتمد على
for (const auto& point : points) {
std::cout << "{" << point.x << ", " << point.y << "}\n";
}

decltype(points[0]) topRightCorner{1, 1};
auto it = std::find(points, points + 4, topRightCorner);
std::cout << "top right corner is the "
<< 1 + std::distance(points, it) << "th\n";
}

أعضاء الصنف الساكنة

يُسمح للصنف أن يكون له أعضاء ساكنة (‎static‎) قد تكون متغيرات أو دوالًا، وتعدُّ هذه الأعضاء في نطاق الصنف لكنها لا تُعامل كأعضاء عاديّة، إذ أنها تتميّز بمدة تخزين ثابتة (تستمرّ في الوجود من بداية البرنامج وحتى نهايته)، ولا ترتبط بنُسخة معيّنة من الصنف، ولا يوجد منها سوى نسخة واحدة للصنف بأكمله.

class Example {
static int num_instances;    // حقل ساكن
int i;                // حقل غير ساكن
    public:
static std::string static_str;    // حقل ساكن
static int static_func();    // تابع ساكن

// يجوز للتوابع غير الساكنة تعديل الحقول الساكنة
    Example() {
        ++num_instances;
    }
    void set_str(const std::string & str);
};

int Example::num_instances;
std::string Example::static_str = "Hello.";

// ...

Example one, two, three
// الخاصة به، مثل ما يلي “i” كل مثال لديه :
//  (&one.i != &two.i)
//  (&one.i != &three.i)
//  (&two.i != &three.i).
// انظر ما يلي ، “num_instances” تتشارك الأمثلة الثلاثة:
//  (&one.num_instances == &two.num_instances)
//  (&one.num_instances == &three.num_instances)
//  (&two.num_instances == &three.num_instances)

لا تُعدُّ الحقول الساكنة مُعرَّفة داخل الصنف ولكنّها تُعدّ مُصرّحة فقط، وعليه سيكون تعريفها خارج تعريف الصنف. يُسمح للمبرمج بتهيئة المتغيّرات الساكنة في التعريف لكنه غير ملزَم بهذا، وعند تعريف المتغيرات العضوية تُحذف الكلمة المفتاحية ‎static‎.

class Example {
    static int num_instances;            // تصريح
    public:
        static std::string static_str;            // تصريح
    // ...
};
int Example::num_instances;            // تعريف مُهيّأ صفريًا.
std::string Example::static_str = "Hello.";        // تعريف  

لهذا السبب، يمكن أن تكون المتغيرات الساكنة أنواعًا غير مكتملة (بخلاف ‎void‎)، طالما عُرِّفت لاحقًا كنوع كامل.

struct ForwardDeclared;

class ExIncomplete {
    static ForwardDeclared fd;
    static ExIncomplete i_contain_myself;
    static int an_array[];
};

struct ForwardDeclared {};

ForwardDeclared    ExIncomplete::fd;
ExIncomplete    ExIncomplete::i_contain_myself;
int            ExIncomplete::an_array[5];

يمكن تعريف الدوال التابعة الساكنة داخل أو خارج تعريف الصنف كما هو حال الدوال التابعة العادية، وكما هو الحال مع المتغيرات العضوية الساكنة، تُحذَف الكلمة المفتاحية ‎static‎ في حال تعريف العضو الساكن خارج تعريف الصنف.

// بالنسبة للمثال أعلاه، إما
class Example {
    // ...
    public:
        static int static_func() {
            return num_instances;
        }
    // ...
    void set_str(const std::string& str) {
        static_str = str;
    }
};
// أو
class Example {
    /* ... */ };
int Example::static_func() {
    return num_instances;
}
void Example::set_str(const std::string& str) {
    static_str = str;
}

في حال التصريح عن حقل على أنه ثابت (‎const‎) وليس متغيّرًا (‎volatile‎)، وكان من نوع عددي صحيح (integral) أو تِعدادي (enumeration)، فيمكن تهيئته عند التصريح عنه داخل تعريف الصنف.

enum E { VAL = 5 };

struct ExConst {
const static int ci = 5;            // جيد.
static const E ce = VAL;            // جيد.
const static double cd = 5;            // خطأ.
static const volatile int cvi = 5;        // خطأ.

const static double good_cd;
static const volatile int good_cvi;
};

const double ExConst::good_cd = 5;        // جيد.
const volatile int ExConst::good_cvi = 5;        // جيد.

الإصدار ≥ C++‎ 11

بدءًا من الإصدار C++‎ 11، يمكن تعريف المتغيرات العضوية الساكنة من الأنواع ‎LiteralType‎ (أنواع يمكن إنشاؤها في وقت التصريف وفقًا لقواعد التعبيرات الثابتة ‎constexpr‎) كتعبيرات ثابتة؛ لكن يجب أن تُهيّأ داخل تعريف الصنف.

struct ExConstexpr {
constexpr static int ci = 5;                    // جيد.
static constexpr double cd = 5;                // جيد.
constexpr static int carr[] = { 1, 1, 2 };            // جيد.
static constexpr ConstexprConstructibleClass c{};    // جيد.
constexpr static int bad_ci; // Error.
};

constexpr int ExConstexpr::bad_ci = 5;                // خطأ كذلك.

في حال تمّ أخذ عنوان (odr-used) لمتغير عضوي ساكن من النوع الثابت (‎const‎) أو ‎constexpr‎، أو عُيَّن إلى مرجع فينبغي أن يكون له تعريف منفصل خارج تعريف الصنف، ولا يُسمح لهذا التعريف باحتواء مُهيّئ.

struct ExODR {
    static
    const int odr_used = 5;
};
// const int ExODR::odr_used;
const int* odr_user = & ExODR::odr_used; // خطأ، ألغ تعليق السطر أعلاه لحل المشكلة

يمكن الوصول إلى الأعضاء الساكنة باستخدام عامل النطاق ‎::‎ بما أنها لأنّها غير مرتبطة بنُسخة معيّنة.

std::string str = Example::static_str;

يمكن أيضًا الوصول إليها كما لو كانت أعضاءً عادية وغير ساكنة، هذا له دلالة تاريخية ولكنه يُستخدم بشكل أقل من عامل النطاق لأنه يمنع أيّ لبس بخصوص ما إذا كان العضو ساكنًا أم لا.

Example ex;
std::string rts = ex.static_str;

يمكن لأعضاء الصنف الوصول إلى الأعضاء الساكنة دون الحاجة إلى تأهيل نطاقهم (qualifying their scope)، كما هو الحال مع أعضاء الصنف غير الساكنة.

class ExTwo {
static int num_instances;
int my_num;

    public:
ExTwo() : my_num(num_instances++) {}

static int get_total_instances() { return num_instances; }
int get_instance_number() const { return my_num; }
};

int ExTwo::num_instances;

لا يمكن أن تكون الأعضاء الساكنة قابلة للتغيير (‎mutable‎)، وليست في حاجة لهذا لأنها غير مرتبطة بأيّ نُسخة، أمّا مسـالة أنّ النُسخة ثابتة أم لا فليس لها أيّ تأثير على الأعضاء الساكنة.

struct ExDontNeedMutable {
    int immuta;
    mutable int muta;

   static int i;

    ExDontNeedMutable(): immuta(-5), muta(-5) {}
};
int ExDontNeedMutable::i;

// ...

const ExDontNeedMutable dnm;
dnm.immuta = 5;    // Error: Can't modify read-only object.
dnm.muta = 5;    // يمكن إعادة كتابة الحقول القابلة للتغيير من كائن ثابت
dnm.i = 5;        // يمكن إعادة كتابة الأعضاء الساكنة بغض النظر عن ثباتيّة النسخة

تحترم الأعضاء الساكنة مُحدِّدات الوصول، تمامًا مثل الأعضاء غير الساكنة.

class ExAccess {
    static int prv_int;

    protected:
        static int pro_int;

    public:
        static int pub_int;
};

int ExAccess::prv_int;
int ExAccess::pro_int;
int ExAccess::pub_int;

// ...

int x1 = ExAccess::prv_int; // خاص int ExAccess::prv_int خطأ، لأنّ
int x2 = ExAccess::pro_int; // محمي int ExAccess::pro_int خطأ، لأنّ
int x3 = ExAccess::pub_int; // جيّد

بحُكم أنّها غير مرتبطة بنُسخة معيّنة، فلا تمتلك التوابع الساكنة المؤشّرُ ‎this‎؛ وعليه لا تستطيع الوصول إلى الحقول غير الساكنة إلا في حال تمرير نُسخة إليها.

class ExInstanceRequired {
int i;

    public:
ExInstanceRequired() : i(0) {}

static void bad_mutate() { ++i *= 5; }                    // خطأ
static void good_mutate(ExInstanceRequired& e) { ++e.i *= 5; }    // جيد
};

ونظرًا لعدم امتلاكها للمؤشّر ‎this‎، فلا يمكن تخزين عناوينها في مؤشّرات الدوال التابعة (pointers-to-member-functions)، وتُخزّن بدلاً من ذلك في مؤشّرات الدّوال (pointers-to-functions) العادية.

struct ExPointer {
    void nsfunc() {}
    static void sfunc() {}
};
typedef void (ExPointer::* mem_f_ptr)();
typedef void( * f_ptr)();
mem_f_ptr p_sf = &ExPointer::sfunc; // خطأ
f_ptr p_sf = &ExPointer::sfunc; // جيد

نظرًا لعدم امتلاكها لمؤشّر ‎this‎، فهي لا ثابتة (‎const‎) ولا متغيّرة (‎volatile‎)، ولا يمكن أن يكون لها مؤهّلات مرجعية (ref-qualifiers). كما لا يمكنها أن تكون وهميّة كذلك.

struct ExCVQualifiersAndVirtual {
    static void func() {}            // جيد
    static void cfunc() const {}        // خطأ
    static void vfunc() volatile {}        // خطأ
    static void cvfunc() const volatile {}    // خطأ
    static void rfunc() & {}            //خطأ 
    static void rvfunc() && {}            // خطأ

    virtual static void vsfunc() {}        // خطأ
    static virtual void svfunc() {}        // خطأ
};

وبما أن المتغيرات العضوية الساكنة لا ترتبط بنُسخة معينة، فيُتعَامل معها كمتغيرات عامّة استثنائيّة (special global variables)؛ فتُنشأ عند بدء تشغيل البرنامج ولا تُحذف حتى إنهائه، بغض النظر عمّا إذا كانت هناك أيّ نُسخ موجودة بالفعل من الصنف. ولا توجد إلا نسخة (copy) واحدة فقط من كل حقل ساكن (ما لم يُصرَّح بالمتغير كـ ‎thread_local‎ - الإصدار C++‎ 11 أو أحدث - وفي هذه الحالة تكون هناك نُسخة واحدة لكل خيط thread).

والمتغيرات العضوية الساكنة لها نفس ارتباط (linkage) الصِّنف، سواء كان للصنف ارتباط خارجي أو داخلي، ولا يُسمح للأصناف المحلية (Local classes) والأصناف غير المُسمّاة (unnamed classes) أن يكون لها أعضاء ساكنة.

الوراثة المتعددة (Multiple Inheritance)

إلى جانب الوراثة الفردية (single inheritance):

class A {};
class B : public A {};

فهناك مفهوم الوراثة المتعددة في C++‎:

class A {};
class B {};
class C : public A, public B {};

سوف يرث ‎C‎ الآن من ‎A‎ و ‎B‎ في الوقت ذاته.

ملاحظة: كن حذرًا إذ قد يؤدّي هذا إلى بعض الغموض في حال استخدام نفس الأسماء في عدّة أصناف أو بنيات موروثة.

الغموض في الوراثة المتعددة

قد تكون الوراثة المتعدّدة مفيدة في بعض الحالات ولكنها قد تخلق بعض المشاكل، فعلى سبيل المثال: لنفترض أنّ صنفين أساسيَن يحتويان على دوال بنفس الاسم، وأنّ الصنف المشتق منهما لم يُعِدْ تعريف تلك الدالة، فإن حاولت الوصول إلى تلك الدالة عبر كائن من الصنف المُشتق، فسيعرِض المُصرّف خطأً لأنه لا يستطيع تحديد الدالة التي يجب استدعاؤها. إليك مثالًا على ذلك:

class base1 {
    public:
        void funtion() { //شيفرة الدالّة }
        };
    class base2 {
        void
        function () { // شيفرة الدالّة }
        };
        class derived: public base1, public base2 {

        };
        int main() {
            derived obj;

            // خطأ، لأنّ المصرف لا يستطيع أن يحدد التابع الذي يجب استدعاؤه
            obj.function()
        }

ولكن يمكن حلّ هذه المشكلة باستخدام دالّة حلّ النطاق (scope resolution function) لتحديد الصنف الذي سيُستدعى تابعه:

int main() {
    obj.base1:: function (); // base1 تُستدعى دالة الصنف
    obj.base2:: function (); // base2 تُستدعى دالة الصنف
}

الدوال التابعة غير الساكنة (Non-static member functions)

يمكن أن يكون للصنف دوال تابعة غير ساكنة تعمل على نُسخ الصنف المستقلة.

class CL {
    public:
        void member_function() {}
};

تُستدعى هذه التوابع على نُسَخ الصنف، على النحو التالي:

CL instance;
instance.member_function();

ويمكن تعريفها إمّا داخل أو خارج تعريف الصنف، فإذا عُرِّفت في الخارج ستُعدُّ داخل نطاق الصنف.

struct ST {
    void defined_inside() {}
    void defined_outside();
};
void ST::defined_outside() {}

ويمكن أن تُؤهّل (CV-qualified) أو تُؤهّل مرجعيًا (ref-qualified)، وينعكس ذلك على كيفية رؤيتها للنُّسخة التي استُدعيت عليها، ويعتمد الإصدار الذي سيُستدعى على المؤهّلات الثباتية للنُّسخة، فإذا لم يكن هناك إصدار له نفس المؤهِّلات الثباتيّة للنُّسخة، فسيُستدعى إصدار أكثر تأهيلًا ثباتيًا (more-cv-qualified) إذا كان متاحًا.

struct CVQualifiers {
void func() {}                    // 1: غير مؤهلة ثباتيا
void func() const {}                // 2:غير ثابتة 
void cv_only() const volatile {}
};
CVQualifiers       non_cv_instance;
const CVQualifiers      c_instance;
non_cv_instance.func();            // #1 استدعاء
c_instance.func();                // #2 استدعاء
non_cv_instance.cv_only();        // const volatile استدعاء الإصدار 
c_instance.cv_only();            // const volatile استدعاء الإصدار

الإصدار ≥ C++‎ 11

تحدّد الدوال التابعة المؤهّلة مرجِعيًّا (Member function ref-qualifiers) ما إذا كان يجوز أن يُستدعَى التابع على نُسخ يمينِيَّة (rvalue) أم لا، وتستخدم نفسَ صياغةِ التوابع المؤهّلة ثباتيًّا.

struct RefQualifiers {
    void func() & {}            // 1: تُستدعى على  النسخ العادية
    void func() && {}            // 2: تُستدعي على النسخ اليمينية
};
RefQualifiers rf;
rf.func();                // #1 استدعاء
RefQualifiers {}.func();        // #2 استدعاء

يجوز دمج المؤهّلات الثباتية والمؤهّلات المرجعية إذا لزم الأمر.

struct BothCVAndRef {
    void func() const & {}        // تُستدعى على  النسخ العادية
    void func() && {}            // تُستدعى على  النسخ المؤقتة
};

يمكن أيضًا أن تكون وهميّة، وهذا ضروري في التعددية الشكلية، ويسمح للأصناف المشتقة بأن يكون لها نفس واجهة الصنف الأب مع إضافة وظائفها الخاصّة.

struct Base {
    virtual void func() {}
};
struct Derived {
    virtual void func() {}
};
Base * bp = new Base;
Base * dp = new Derived;
bp.func(); // Base::func() استدعاء  
dp.func(); // Derived::func() استدعاء

هذا الدرس جزء من سلسلة دروس عن C++‎.

ترجمة -وبتصرّف- للفصل Chapter 34: Classes/Structures من الكتاب C++ Notes for Professionals





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن