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


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

15 734

15 ответов:

чтобы расширить ответ на 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
            }
    }
    

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

перепутал?


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

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

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

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

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

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

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

наконец, наследование является корнем всего зла в сложном программном обеспечении. В семантика значения Шона родителя и основанный на понятиях полиморфизм (настоятельно рекомендуется, там объясняются лучшие варианты этой техники) изучается следующий случай:

скажем, у меня есть приложение, в котором я имею дело с моими формами полиморфно помощью 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 интерфейс... что произойдет, если они также вызовут свою функцию draw my_draw ?).

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

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

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

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

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

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

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

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

вы также можете рассмотреть классы контрактов, реализованные с помощью 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.
};

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

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

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

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

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

спасибо.

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

советы:

  • вы можете унаследовать от это как базовый класс (разрешены как виртуальные, так и невиртуальные) и fill click в конструкторе потомка.
  • у вас может быть указатель функции как protected - члены и public ссылка и/или геттер.
  • как уже упоминалось выше, это позволяет переключать реализацию во время выполнения. Таким образом, это способ управлять государством. В зависимости от количества if s против изменения состояния в коде, это может быстрее switch() es или ifs (оборот ожидается около 3-4 ifs, но всегда измеряйте сначала.
  • если вы выберите std::function<> над указателями функций, вы может иметь возможность управлять всеми данными объекта в пределах IBase. С этого момента вы можете иметь схемы значений для IBase (например, std::vector<IBase> будет работать). Обратите внимание, что это может быть медленнее в зависимости от вашего компилятора и кода STL; также, что текущие реализации std::function<> как правило, накладные расходы, когда по сравнению с указателями функций или даже виртуальными функциями (это может измениться в будущем).

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

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