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

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

18 ответов

Решение

Чтобы расширить ответ с помощью 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
        }
};

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

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

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

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

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

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

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

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

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

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

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

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

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

Насколько я мог проверить, очень важно добавить виртуальный деструктор. Я использую объекты, созданные с 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() никогда не называется.

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

  1. Объявите виртуальный деструктор в вашем интерфейсе или создайте защищенный не виртуальный, чтобы избежать неопределенного поведения, если кто-то пытается удалить объект типа 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++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?).

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

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

Смущенный?


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

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

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

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

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

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

Вы также можете рассмотреть классы контрактов, реализованные с помощью NVI (Non Virtual Interface Pattern). Например:

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

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

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

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

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

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

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

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

В C ++ 20 вы можете использовать conceptвместо класса. Это более эффективно, чем наследование.

      template <class T>
concept MyInterface = requires (T t) {
    { t.interfaceMethod() };
};

class Implementation {
public:
    void interfaceMethod();
};
static_assert(MyInterface<Implementation>);

Затем вы можете использовать его в функции:

      void myFunction(MyInterface auto& arg);

Ограничение в том, что вы не можете использовать его в контейнере.

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

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

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

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

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

Подсказки:

  • Вы можете наследовать от него как базовый класс (как виртуальный, так и не виртуальный), и заполнить click в конструкторе вашего потомка.
  • Вы можете иметь указатель на функцию в виде protected член и есть public ссылка и / или получатель.
  • Как упоминалось выше, это позволяет переключать реализацию во время выполнения. Таким образом, это способ управления государством. В зависимости от количества ifИзменения в зависимости от состояния в вашем коде, это может быть быстрее, чем switch()или ifс (поворот ожидается около 3-4 ifs, но всегда измеряйте первым.
  • Если вы выбираете std::function<> С помощью указателей функций вы можете управлять всеми данными вашего объекта в пределах IBase, С этого момента вы можете иметь схемы значений для IBase (например, std::vector<IBase> буду работать). Обратите внимание, что это может быть медленнее в зависимости от вашего компилятора и кода STL; также, что текущие реализации std::function<> обычно имеют накладные расходы по сравнению с указателями на функции или даже виртуальными функциями (это может измениться в будущем).
      The C++ interfaces are implemented using abstract classes and these abstract classes should not be confused with data abstraction which is a concept of keeping implementation details separate from associated data.

Example:

class Shape {
   public:
      // pure virtual function providing interface framework.
      virtual int getArea() = 0;
      virtual ~Shape(){}
          
   protected:
      int width;
      int height;
};
 
// Derived classes
class Rectangle: public Shape {
   public:
      int getArea() { 
         return (width * height); 
      }
};

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

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

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

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

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

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

Благодарю.

Если вам нужна только статическая привязка интерфейса (без виртуального, без экземпляров самого типа интерфейса, интерфейс действует только как руководство):

      #include <iostream>
#include <string>

// Static binding interface
// Notice: instantiation of this interface should be usefuless and forbidden.
class IBase {
 protected:
  IBase() = default;
  ~IBase() = default;

 public:
  // Methods that must be implemented by the derived class
  void behaviorA();
  void behaviorB();

  void behaviorC() {
    std::cout << "This is an interface default implementation of bC().\n";
  };
};

class CCom : public IBase {
  std::string name_;

 public:
  void behaviorA() { std::cout << "CCom bA called.\n"; };
};

class CDept : public IBase {
  int ele_;

 public:
  void behaviorB() { std::cout << "CDept bB called.\n"; };
  void behaviorC() {
    // Overwrite the interface default implementation
    std::cout << "CDept bC called.\n";
    IBase::behaviorC();
  };
};

int main(void) {
  // Forbid the instantiation of the interface type itself.
  // GCC error: ‘constexpr IBase::IBase()’ is protected within this context
  // IBase o;

  CCom acom;
  // If you want to use these interface methods, you need to implement them in
  // your derived class. This is controled by the interface definition.
  acom.behaviorA();
  // ld: undefined reference to `IBase::behaviorB()'
  // acom.behaviorB();
  acom.behaviorC();

  CDept adept;
  // adept.behaviorA();
  adept.behaviorB();
  adept.behaviorC();
  // adept.IBase::behaviorC();
}

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

n4687

13.4.2

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

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(), и два других класса реализовали одну и ту же функцию, но с другим алгоритмом для вычисления области, специфичной для фигуры.

Другие вопросы по тегам