geeksforgeeks - polymorphism in c++




كيف تعلن واجهة في C++؟ (10)

إجابتي هي في الأساس نفس إجابات الآخرين ولكن أعتقد أن هناك شيئين مهمين آخرين يجب القيام بهما:

  1. إعلان destructor الظاهري في واجهة أو إجراء واحد غير ظاهري محمية لتجنب السلوكيات غير معرفة إذا حاول شخص ما حذف كائن من نوع IDemo .

  2. استخدام الوراثة الافتراضية لتجنب المشاكل التي تعدد الميراث. (هناك في كثير من الأحيان تعدد الميراث عند استخدام الواجهات.)

ومثل الإجابات الأخرى:

  • جعل فئة مع وسائل افتراضية نقية.
  • استخدم الواجهة عن طريق إنشاء فصل آخر يتجاوز تلك الطرق الافتراضية.

    class IDemo
    {
        public:
            virtual void OverrideMe() = 0;
            virtual ~IDemo() {}
    }
    

    أو

    class IDemo
    {
        public:
            virtual void OverrideMe() = 0;
        protected:
            ~IDemo() {}
    }
    

    و

    class Child : virtual public IDemo
    {
        public:
            virtual void OverrideMe()
            {
                //do stuff
            }
    }
    

كيف أقوم بإعداد فصل دراسي يمثل واجهة؟ هل هذا مجرد طبقة أساسية مجردة؟


إذا كنت تستخدم مترجم C ++ الخاص بـ Microsoft ، فيمكنك القيام بما يلي:

struct __declspec(novtable) IFoo
{
    virtual void Bar() = 0;
};

class Child : public IFoo
{
public:
    virtual void Bar() override { /* Do Something */ }
}

يعجبني هذا الأسلوب لأنه ينتج عنه رمز واجهة أصغر كثيرًا ويمكن أن يكون حجم الشفرة الذي تم إنشاؤه أصغر كثيرًا. يزيل استخدام novtable كل مرجع إلى مؤشر vtable في تلك الفئة ، بحيث لا يمكنك أبدًا إنشاءه مباشرة. انظر الوثائق هنا - novtable .


السبب الكامل لوجود فئة خاصة من نوع الواجهة بالإضافة إلى فئات أساسية مجردة في C # / Java لأن C # / Java لا يدعم الوراثة المتعددة.

يدعم C ++ تواريث متعددة ، ولذلك لا يحتاج الأمر إلى نوع خاص. فئة أساسية مجردة مع عدم وجود طرق غير مجردة (افتراضية نقية) تكافئ وظيفيا لواجهة C # / Java.


بقدر ما يمكنني اختبار ، من المهم للغاية إضافة destructor الظاهري. أنا أستخدم كائنات تم إنشاؤها باستخدام new ودمرت مع delete .

إذا لم تقم بإضافة destructor الظاهري في الواجهة ، ثم يتم استدعاء destructor للفئة الموروثة.

class IBase {
public:
    virtual ~IBase() {}; // destructor, use it to call destructor of the inherit classes
    virtual void Describe() = 0; // pure virtual method
};

class Tester : public IBase {
public:
    Tester(std::string name);
    virtual ~Tester();
    virtual void Describe();
private:
    std::string privatename;
};

Tester::Tester(std::string name) {
    std::cout << "Tester constructor" << std::endl;
    this->privatename = name;
}

Tester::~Tester() {
    std::cout << "Tester destructor" << std::endl;
}

void Tester::Describe() {
    std::cout << "I'm Tester [" << this->privatename << "]" << std::endl;
}


void descriptor(IBase * obj) {
    obj->Describe();
}

int main(int argc, char** argv) {

    std::cout << std::endl << "Tester Testing..." << std::endl;
    Tester * obj1 = new Tester("Declared with Tester");
    descriptor(obj1);
    delete obj1;

    std::cout << std::endl << "IBase Testing..." << std::endl;
    IBase * obj2 = new Tester("Declared with IBase");
    descriptor(obj2);
    delete obj2;

    // this is a bad usage of the object since it is created with "new" but there are no "delete"
    std::cout << std::endl << "Tester not defined..." << std::endl;
    descriptor(new Tester("Not defined"));


    return 0;
}

إذا قمت بتشغيل التعليمات البرمجية السابقة دون virtual ~IBase() {}; ، سترى أن the destructor Tester::~Tester() لا يسمى أبدا.


جعل فئة مع وسائل افتراضية نقية. استخدم الواجهة عن طريق إنشاء فصل آخر يتجاوز تلك الطرق الافتراضية.

الطريقة الافتراضية الخالصة هي طريقة الفئة التي يتم تعريفها على أنها افتراضية ويتم تعيينها إلى 0.

class IDemo
{
    public:
        virtual ~IDemo() {}
        virtual void OverrideMe() = 0;
};

class Child : public IDemo
{
    public:
        virtual void OverrideMe()
        {
            //do stuff
        }
};

في C ++ 11 ، يمكنك بسهولة تجنب التوريث تمامًا:

struct Interface {
  explicit Interface(SomeType& other)
  : foo([=](){ return other.my_foo(); }), 
    bar([=](){ return other.my_bar(); }), /*...*/ {}
  explicit Interface(SomeOtherType& other)
  : foo([=](){ return other.some_foo(); }), 
    bar([=](){ return other.some_bar(); }), /*...*/ {}
  // you can add more types here...

  // or use a generic constructor:
  template<class T>
  explicit Interface(T& other)
  : foo([=](){ return other.foo(); }), 
    bar([=](){ return other.bar(); }), /*...*/ {}

  const std::function<void(std::string)> foo;
  const std::function<void(std::string)> bar;
  // ...
};

في هذه الحالة ، تحتوي واجهة على دلالات مرجعية ، أي يجب عليك التأكد من أن الكائن يتجاوز الواجهة (من الممكن أيضًا عمل واجهات مع دلالات القيمة).

هذا النوع من الواجهات له مزاياه وسلبياته:

  • أنها تتطلب ذاكرة أكثر من تعدد الأشكال القائم على الوراثة.
  • هم بشكل عام أسرع من التعدد القائم على التوارث.
  • في تلك الحالات التي تعرف فيها النوع النهائي ، فهي أسرع بكثير! (يقوم بعض المترجمين مثل gcc و clang بإجراء المزيد من التحسينات في الأنواع التي ليس لديها / ترث من الأنواع ذات الوظائف الظاهرية).

أخيرا ، الميراث هو أصل كل الشرور في تصميم البرمجيات المعقدة. في علم الدلالة القيمية لـ Sean Parent و تعدد الأشكال المبني على المفاهيم (الموصى به بشدة ، يتم شرح إصدارات أفضل من هذه التقنية هناك) يتم دراسة الحالة التالية:

قل لدي تطبيق أتعامل فيه مع الأشكال الخاصة بي بشكل متعدد الأشكال باستخدام واجهة MyShape :

struct MyShape { virtual void my_draw() = 0; };
struct Circle : MyShape { void my_draw() { /* ... */ } };
// more shapes: e.g. triangle

في التطبيق الخاص بك ، تفعل الشيء نفسه مع الأشكال المختلفة باستخدام واجهة YourShape :

struct YourShape { virtual void your_draw() = 0; };
struct Square : YourShape { void your_draw() { /* ... */ } };
/// some more shapes here...

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

struct Circle : MyShape, YourShape { 
  void my_draw() { /*stays the same*/ };
  void your_draw() { my_draw(); }
};

أولاً ، قد لا يكون تعديل الأشكال الخاصة بي ممكنًا على الإطلاق. وعلاوة على ذلك ، فإن الميراث المتعدد يقود الطريق إلى كود السباغيتي (تخيل أن مشروع ثالث يأتي في ذلك باستخدام واجهة TheirShape ... ماذا يحدث إذا قاموا أيضا باستدعاء وظيفة السحب الخاصة بهم my_draw ؟).

تحديث: هناك اثنين من المراجع الجديدة حول تعدد الأشكال القائم على غير الوراثة:


لا يوجد مفهوم "واجهة" في حد ذاتها في C ++. قدمت AFAIK ، واجهات لأول مرة في جاوة للعمل حول عدم وجود الميراث متعددة. لقد تبين أن هذا المفهوم مفيد للغاية ، ويمكن تحقيق نفس التأثير في C ++ باستخدام طبقة أساسية مجردة.

الفئة الأساسية التجريدية هي فئة فيها وظيفة عضو واحد على الأقل (طريقة في لغة جافا) هي دالة ظاهرية خالصة يتم الإعلان عنها باستخدام البنية التالية:

class A
{
  virtual void foo() = 0;
};

لا يمكن إنشاء فئة أساسية مجردة ، أي لا يمكنك تعريف كائن من الفئة A. يمكنك فقط استخلاص الفئات من A ، ولكن أي فئة مشتقة لا توفر تطبيق foo() ستكون أيضًا مجردة. من أجل التوقف عن التجريد ، يجب أن توفر فئة مشتقة تطبيقات لكل الوظائف الظاهرية الخالصة التي ترثها.

لاحظ أن فئة أساسية مجردة يمكن أن تكون أكثر من واجهة ، لأنها يمكن أن تحتوي على أعضاء البيانات ووظائف الأعضاء التي ليست محض افتراضية. أي ما يعادل واجهة سيكون فئة أساسية مجردة دون أي بيانات مع وظائف افتراضية نقية فقط.

وكما أشار مارك رانسوم ، يجب أن توفر طبقة أساسية مجردة دمارًا افتراضيًا ، تمامًا مثل أي طبقة أساسية ، لهذه المسألة.


للتوسع في الإجابة عن طريق bradtgmurray ، قد ترغب في إجراء استثناء واحد لقائمة الطريقة الظاهرية الخاصة بالواجهة عن طريق إضافة destructor افتراضي. هذا يسمح لك بتمرير الملكية إلى طرف آخر دون تعريض الطبقة المشتقة من الخرسانة. لا يحتاج المدمر إلى فعل أي شيء ، لأن الواجهة لا تحتوي على أي أعضاء ملموسين. قد يبدو من المتناقض تحديد وظيفة افتراضية ومضمنة ، لكن ثق بي - إنها ليست كذلك.

class IDemo
{
    public:
        virtual ~IDemo() {}
        virtual void OverrideMe() = 0;
};

class Parent
{
    public:
        virtual ~Parent();
};

class Child : public Parent, public IDemo
{
    public:
        virtual void OverrideMe()
        {
            //do stuff
        }
};

لا يجب عليك تضمين جسد المدمر الافتراضي - فقد اتضح أن بعض المترجمين لديهم مشكلة في تحسين وجود مدمر فارغ وأنك أفضل حالا باستخدام الافتراضي.


هنا تعريف abstract class في معيار c ++

n4687

13.4.2

الفئة التجريدية هي فئة يمكن استخدامها فقط كفئة أساسية لبعض الصفوف الأخرى ؛ لا يمكن إنشاء كائنات من فئة مجردة باستثناء عناصر فرعية لفئة مشتقة منه. الفئة مجردة إذا كان لديها وظيفة افتراضية واحدة على الأقل.


يمكنك أيضًا دراسة فصول العقود التي يتم تنفيذها مع NVI (نمط واجهة غير ظاهري). على سبيل المثال:

struct Contract1 : boost::noncopyable
{
    virtual ~Contract1();
    void f(Parameters p) {
        assert(checkFPreconditions(p)&&"Contract1::f, pre-condition failure");
        // + class invariants.
        do_f(p);
        // Check post-conditions + class invariants.
    }
private:
    virtual void do_f(Parameters p) = 0;
};
...
class Concrete : public Contract1, public Contract2
{
private:
    virtual void do_f(Parameters p); // From contract 1.
    virtual void do_g(Parameters p); // From contract 2.
};




pure-virtual