Как вы объявляете интерфейс в C ++?

768

Как настроить класс, представляющий интерфейс? Это просто абстрактный базовый класс?

Теги:
inheritance
interface
abstract-class
pure-virtual

15 ответов

600
Лучший ответ

Чтобы расширить ответ на bradtgmurray, вы можете сделать одно исключение из списка чистого виртуального метода вашего интерфейса, добавив виртуальный деструктор. Это позволяет вам передать владение указателем другой стороне, не подвергая конкретный производный класс. Деструктор не должен ничего делать, потому что интерфейс не имеет каких-либо конкретных элементов. Может показаться противоречивым определение функции как виртуальной, так и встроенной, но поверьте мне - это не так.

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
        }
};

Вам не нужно включать тело для виртуального деструктора - оказывается, некоторые компиляторы не могут оптимизировать пустой деструктор, и вам лучше использовать значение по умолчанию.

  • 102
    Виртуальный desctuctor ++! Это очень важно. Вы также можете включить чисто виртуальные объявления определений оператора = и скопировать определения конструктора, чтобы компилятор не генерировал их автоматически.
  • 31
    Альтернативой виртуальному деструктору является защищенный деструктор. Это отключает полиморфное разрушение, которое может быть более подходящим в некоторых обстоятельствах. Ищите « Положение № 4» в gotw.ca/publications/mill18.htm .
Показать ещё 26 комментариев
237

Создайте класс с помощью виртуальных методов. Используйте интерфейс, создав еще один класс, который переопределяет эти виртуальные методы.

Чистым виртуальным методом является метод класса, который определяется как виртуальный и назначается 0.

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

class Child : public IDemo
{
    public:
        virtual void OverrideMe()
        {
            //do stuff
        }
};
  • 29
    у вас должен быть деструктор бездействия в IDemo, чтобы его поведение определялось: IDemo * p = new Child; / * что угодно * / удалить p;
  • 11
    Почему метод OverrideMe в дочернем классе является виртуальным? Это необходимо?
Показать ещё 5 комментариев
124

Вся причина, по которой у вас есть специальная категория типа интерфейса в дополнение к абстрактным базовым классам в С#/Java, заключается в том, что С#/Java не поддерживают множественное наследование,

С++ поддерживает множественное наследование, поэтому особый тип не нужен. Абстрактный базовый класс без не абстрактных (чисто виртуальных) методов функционально эквивалентен интерфейсу С#/Java.

  • 17
    Было бы неплохо иметь возможность создавать интерфейсы, чтобы избавить нас от необходимости много печатать (virtual, = 0, virtual destructor). Кроме того, множественное наследование кажется мне очень плохой идеей, и я никогда не видел, чтобы оно использовалось на практике, но интерфейсы нужны постоянно. К сожалению, C ++ не представит интерфейсы только потому, что я хочу их.
  • 8
    Ha11owed: у него есть интерфейсы. Они называются классами с чисто виртуальными методами и без реализаций методов.
Показать ещё 12 комментариев
42

В С++ нет понятия "интерфейс" как таковой. AFAIK, интерфейсы были впервые представлены на Java, чтобы обойти отсутствие множественного наследования. Эта концепция оказалась весьма полезной, и такой же эффект можно достичь в С++ с использованием абстрактного базового класса.

Абстрактный базовый класс - это класс, в котором по крайней мере одна функция-член (метод в Java lingo) является чистой виртуальной функцией, объявленной с использованием следующего синтаксиса:

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

Абстрактный базовый класс не может быть создан, т.е. е. вы не можете объявить объект класса A. Вы можете выводить только классы из A, но любой производный класс, который не обеспечивает реализацию foo(), также будет абстрактным. Чтобы перестать быть абстрактным, производный класс должен обеспечивать реализации для всех чистых виртуальных функций, которые он наследует.

Обратите внимание, что абстрактный базовый класс может быть больше, чем интерфейс, поскольку он может содержать члены данных и функции-члены, которые не являются чисто виртуальными. Эквивалент интерфейса будет абстрактным базовым классом без каких-либо данных только с чистыми виртуальными функциями.

И, как отметил Марк Рэнсом, абстрактный базовый класс должен предоставлять виртуальный деструктор, как и любой базовый класс.

  • 13
    Больше, чем «отсутствие множественного наследования», я бы сказал, чтобы заменить множественное наследование. Java с самого начала создавалась таким образом, потому что множественное наследование создает больше проблем, чем решает. Хороший ответ
  • 11
    Оскар, это зависит от того, являетесь ли вы программистом C ++, который изучил Java, или наоборот. :) ИМХО, если использовать разумно, как и почти все в C ++, множественное наследование решает проблемы. Абстрактный интерфейсный базовый класс является примером очень разумного использования множественного наследования.
Показать ещё 5 комментариев
38

Насколько я могу проверить, очень важно добавить виртуальный деструктор. Я использую объекты, созданные с помощью new и уничтоженные с помощью delete.

Если вы не добавляете виртуальный деструктор в интерфейс, тогда деструктор унаследованного класса не вызывается.

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() {};, вы увидите, что деструктор Tester::~Tester() никогда не вызывается.

  • 3
    Лучший ответ на этой странице, потому что он дает практический, компилируемый пример. Ура!
  • 1
    Testet :: ~ Tester () запускается, только когда объект "Объявлен с помощью Tester".
Показать ещё 1 комментарий
30

Мой ответ в основном такой же, как и у других, но я думаю, что есть еще две важные вещи:

  • Объявить виртуальный деструктор в вашем интерфейсе или сделать защищенный не виртуальный, чтобы избежать поведения undefined, если кто-то пытается удалить объект типа IDemo.

  • Используйте виртуальное наследование, чтобы избежать проблем с множественным наследованием. (При использовании интерфейсов чаще применяется множественное наследование.)

И как другие ответы:

  • Создайте класс с помощью виртуальных методов.
  • Используйте интерфейс, создав еще один класс, который переопределяет эти виртуальные методы.

    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
            }
    }
    
  • 5
    +1 за упоминание виртуального наследства.
  • 2
    нет необходимости в виртуальном наследовании, поскольку у вас нет элементов данных в интерфейсе.
Показать ещё 4 комментария
9

Все хорошие ответы выше. Еще одна вещь, которую вы должны иметь в виду - вы также можете иметь чистый виртуальный деструктор. Единственное различие заключается в том, что вам все равно нужно его реализовать.

Confused?


    --- header file ----
    class foo {
    public:
      foo() {;}
      virtual ~foo() = 0;

      virtual bool overrideMe() {return false;}
    };

    ---- source ----
    foo::~foo()
    {
    }

Основная причина, по которой вы хотели бы это сделать, - это предоставить вам методы интерфейса, как и у меня, но сделать их необязательными.

Для того, чтобы класс класса интерфейса нуждался в чистом виртуальном методе, но все ваши виртуальные методы имеют стандартные реализации, поэтому единственным способом, который должен сделать чистый виртуальный, является деструктор.

Повторное выполнение деструктора в производном классе вообще не имеет большого значения - я всегда переопределяю деструктор, виртуальный или нет, в моих производных классах.

  • 4
    Почему, ну почему кто-то хочет сделать dtor в этом случае чисто виртуальным? Какую выгоду это принесет? Вы просто навязали бы что-то на производные классы, которые они, вероятно, не должны включать - dtor.
  • 5
    Обновил мой ответ, чтобы ответить на ваш вопрос. Чистый виртуальный деструктор является правильным способом достижения (единственный способ достичь?) Класса интерфейса, где все методы имеют реализации по умолчанию.
8

В С++ 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 Value Semantics и основанный на концепциях полиморфизм (настоятельно рекомендуется, лучшие варианты этой методики объясняются там) изучается следующий случай:

Скажем, у меня есть приложение, в котором я обрабатываю свои фигуры полиморфно с помощью интерфейса 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?).

Обновление: Есть несколько новых ссылок о полиморфизме, основанном на наследовании:

  • 5
    Наследование TBH гораздо яснее, чем то, что есть в C ++ 11, которое притворяется интерфейсом, но скорее связывает некоторые противоречивые конструкции. Пример формы оторван от реальности, а класс Circle - плохой дизайн. Вы должны использовать шаблон Adapter в таких случаях. Извините, если это будет звучать немного грубо, но попробуйте использовать некоторую реальную библиотеку, такую как Qt прежде чем выносить суждения о наследовании. Наследование делает жизнь намного проще.
  • 2
    Это не звучит резко на всех. Как пример формы оторван от реальности? Не могли бы вы привести пример (возможно, на ideone) исправления круга с помощью шаблона Adapter ? Мне интересно увидеть его преимущества.
Показать ещё 19 комментариев
7

Если вы используете компилятор Microsoft С++, вы можете сделать следующее:

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

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

Мне нравится этот подход, потому что он приводит к значительному меньшему интерфейсу кода, а сгенерированный размер кода может быть значительно меньше. Использование novtable удаляет всю ссылку на указатель vtable в этом классе, поэтому вы никогда не сможете его напрямую создать. См. Документацию здесь - novtable.

  • 4
    Я не совсем понимаю, почему вы использовали novtable стандартного virtual void Bar() = 0;
  • 2
    Это в дополнение к (я только что заметил отсутствующий = 0; который я добавил). Прочтите документацию, если вы ее не понимаете.
Показать ещё 1 комментарий
4

Небольшое дополнение к тому, что там написано:

Сначала убедитесь, что ваш деструктор также является чистым виртуальным

Во-вторых, вы можете захотеть наследовать практически (а не обычно), когда вы это реализуете, только для хороших мер.

  • 0
    Зачем наследовать практически?
  • 0
    Мне нравится виртуальное наследование, потому что концептуально это означает, что существует только один экземпляр унаследованного класса. По общему признанию, у класса здесь нет никаких требований к пространству, поэтому он может быть лишним. Я давно не занимался MI в C ++, но разве невиртуальное наследование не усложнит апкастинг?
Показать ещё 5 комментариев
4

Вы также можете рассмотреть классы контрактов, реализованные с 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.
};
  • 0
    Для других читателей эта статья доктора Доббса «Беседы: виртуально ваши» Джима Хислопа и Херба Саттера более подробно раскрывает, почему можно использовать NVI.
  • 0
    А также эта статья "Виртуальность" Херба Саттера.
1

Я все еще новичок в разработке на С++. Я начал работу с Visual Studio (VS).

Однако никто не упоминает __interface в VS (. NET). Я не уверен, что это хороший способ объявить интерфейс. Но, похоже, это обеспечивает дополнительное правоприменение (см. документы). Таким образом, вам не нужно явно указывать virtual TYPE Method() = 0;, поскольку он будет автоматически преобразован.

__interface IMyInterface {
   HRESULT CommitX();
   HRESULT get_X(BSTR* pbstrName);
};

Однако я не использую его, потому что я обеспокоен совместимостью кросс-платформенной компиляции, поскольку он доступен только в .NET.

Если у кого-нибудь есть что-то интересное, пожалуйста, поделитесь.: -)

Спасибо.

0

Вот определение abstract class в стандарте С++

n4687

13.4.2

Абстрактный класс - это класс, который может использоваться только как базовый класс какого-либо другого класса; нет объектов абстрактного класс может быть создан, кроме как подобъекты производного от него класса. Класс является абстрактным, если он имеет хотя бы одна чистая виртуальная функция.

0

Хотя верно, что virtual является де-факто стандартом для определения интерфейса, не забывайте о классическом C-образном шаблоне, который поставляется с конструктором в С++:

struct IButton
{
    void (*click)(); // might be std::function(void()) if you prefer

    IButton( void (*click_)() )
    : click(click_)
    {
    }
};

// call as:
// (button.*click)();

Это имеет то преимущество, что вы можете повторно связать время выполнения событий, не создавая свой класс еще раз (поскольку С++ не имеет синтаксиса для изменения полиморфных типов, это временное решение для классов хамелеона).

Советов:

  • Вы можете наследовать это как базовый класс (разрешены как виртуальные, так и не виртуальные) и заполнить click в вашем потомком-конструкторе.
  • У вас может быть указатель на функцию как член protected и иметь ссылку public и/или getter.
  • Как уже упоминалось выше, это позволяет переключать реализацию во время выполнения. Таким образом, это способ управлять государством. В зависимости от количества if vs. изменения состояния в вашем коде, это может быть быстрее, чем switch() es или if (поворот ожидается примерно на 3-4 if s, но всегда измеряется первым.
  • Если вы выберете std::function<> над указателями функций, вы сможете управлять всеми своими объектными данными в IBase. С этого момента вы можете иметь схемы значений для IBase (например, std::vector<IBase> будет работать). Обратите внимание, что это может быть медленнее в зависимости от вашего компилятора и кода STL; также, что текущие реализации std::function<> имеют тенденцию иметь накладные расходы по сравнению с указателями функций или даже виртуальными функциями (это может измениться в будущем).
-1
class Shape 
{
public:
   // pure virtual function providing interface framework.
   virtual int getArea() = 0;
   void setWidth(int w)
   {
      width = w;
   }
   void setHeight(int h)
   {
      height = h;
   }
protected:
    int width;
    int height;
};

class Rectangle: public Shape
{
public:
    int getArea()
    { 
        return (width * height); 
    }
};
class Triangle: public Shape
{
public:
    int getArea()
    { 
        return (width * height)/2; 
    }
};

int main(void)
{
     Rectangle Rect;
     Triangle  Tri;

     Rect.setWidth(5);
     Rect.setHeight(7);

     cout << "Rectangle area: " << Rect.getArea() << endl;

     Tri.setWidth(5);
     Tri.setHeight(7);

     cout << "Triangle area: " << Tri.getArea() << endl; 

     return 0;
}

Результат: Область прямоугольника: 35 Площадь треугольника: 17

Мы видели, как абстрактный класс определял интерфейс в терминах getArea(), а два других класса реализовали одну и ту же функцию, но с другим алгоритмом для вычисления области, специфичной для формы.

  • 5
    Это не то, что считается интерфейсом! Это просто абстрактный базовый класс с одним методом, который нужно переопределить! Интерфейсы, как правило, представляют собой объекты, которые содержат только определения методов - «контракт», который должны выполнять другие классы при реализации интерфейса.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню