Как вы объявляете интерфейс в C ++? - PullRequest
771 голосов
/ 25 ноября 2008

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

Ответы [ 15 ]

661 голосов
/ 25 ноября 2008

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

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

233 голосов
/ 25 ноября 2008

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

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

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

class Child : public IDemo
{
    public:
        virtual void OverrideMe()
        {
            //do stuff
        }
};
144 голосов
/ 25 ноября 2008

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

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

47 голосов
/ 25 ноября 2008

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

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

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

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

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

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

42 голосов
/ 05 марта 2012

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

32 голосов
/ 25 ноября 2008

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

  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
            }
    }
    
10 голосов
/ 25 июня 2013

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

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

9 голосов
/ 26 ноября 2008

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

Confused?


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

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

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

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

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

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

7 голосов
/ 13 октября 2009

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

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

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

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

4 голосов
/ 25 ноября 2008

Можно также рассмотреть классы контрактов, реализованные с помощью 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.
};
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...