Как я могу симулировать интерфейсы в C++?

Так как C++ не хватает interface особенность Java и C#, что является предпочтительным способом моделирования интерфейсов в классах C++? Мое предположение было бы множественное наследование абстрактных классов. Каковы последствия с точки зрения затрат памяти / производительности? Существуют ли соглашения об именах для таких смоделированных интерфейсов, таких как SerializableInterface?

8 ответов

Решение

Поскольку C++ имеет множественное наследование в отличие от C# и Java, да, вы можете создать серию абстрактных классов.

Что касается соглашения, это зависит от вас; тем не менее, мне нравится ставить имена классов перед I.

class IStringNotifier
{
public:
  virtual void sendMessage(std::string &strMessage) = 0;
  virtual ~IStringNotifier() { }
};

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

На самом деле не нужно "имитировать" что-либо, поскольку в C++ отсутствует то, что Java может делать с интерфейсами.

С точки зрения C++, Java делает "искусственное" различие между interface и class, interface это просто class все методы которого являются абстрактными и которые не могут содержать никаких элементов данных.

Java налагает это ограничение, поскольку не допускает множественного наследования без ограничений, но допускает class в implement несколько интерфейсов.

В C++ class это class и interface это class, extends достигается путем общественного наследования и implements также достигается путем публичного наследования.

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

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

"Каковы последствия с точки зрения затрат памяти / производительности?"

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

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

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

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

С точки зрения производительности, вызов виртуальной функции имеет немного больше накладных расходов, чем вызов не виртуальной функции, и, что более важно, вы можете предположить, что он обычно (всегда?) Не будет встроенным. Добавление пустого базового класса обычно не добавляет никакого кода в конструкцию или уничтожение, потому что пустой базовый конструктор и деструктор могут быть встроены в производный код конструктора / деструктора класса.

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

Пример кода:

#include <iostream>

// A is an interface
struct A {
    virtual ~A() {};
    virtual int a(int) = 0;
};

// B is an interface
struct B {
    virtual ~B() {};
    virtual int b(int) = 0;
};

// C has no interfaces, but does have a virtual member function
struct C {
    ~C() {}
    int c;
    virtual int getc(int) { return c; }
};

// D has one interface
struct D : public A {
    ~D() {}
    int d;
    int a(int) { return d; }
};

// E has two interfaces
struct E : public A, public B{
    ~E() {}
    int e;
    int a(int) { return e; }
    int b(int) { return e; }
};

int main() {
    E e; D d; C c;
    std::cout << "A : " << sizeof(A) << "\n";
    std::cout << "B : " << sizeof(B) << "\n";
    std::cout << "C : " << sizeof(C) << "\n";
    std::cout << "D : " << sizeof(D) << "\n";
    std::cout << "E : " << sizeof(E) << "\n";
}

Выход (GCC на 32-битной платформе):

A : 4
B : 4
C : 8
D : 8
E : 12

Интерфейсы в C++ - это классы, которые имеют только чисто виртуальные функции. Например:

class ISerializable
{
public:
    virtual ~ISerializable() = 0;
    virtual void  serialize( stream& target ) = 0;
};

Это не имитируемый интерфейс, это интерфейс, подобный Java-интерфейсу, но он не имеет недостатков.

Например, вы можете добавлять методы и члены без негативных последствий:

class ISerializable
{
public:
    virtual ~ISerializable() = 0;
    virtual void  serialize( stream& target ) = 0;
protected:
    void  serialize_atomic( int i, stream& t );
    bool  serialized;
};

К соглашениям об именах... в языке C++ не существует реальных соглашений об именах. Так что выберите тот, который в вашей среде.

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

В C++ мы можем пойти дальше, чем простые интерфейсы Java & co. Мы можем добавить явные контракты (как в Design by Contract) с паттерном NVI.

struct Contract1 : noncopyable
{
    virtual ~Contract1();
    Res f(Param p) {
        assert(f_precondition(p) && "C1::f precondition failed");
        const Res r = do_f(p);
        assert(f_postcondition(p,r) && "C1::f postcondition failed");
        return r;
    }
private:
    virtual Res do_f(Param p) = 0;
};

struct Concrete : virtual Contract1, virtual Contract2
{
    ...
};

Интерфейсы в C++ также могут появляться статически, документируя требования к параметрам типа шаблона.

Шаблоны соответствуют синтаксису шаблонов, поэтому вам не нужно заранее указывать , что конкретный тип реализует определенный интерфейс, если у него есть правильные члены. Это в отличие от Java <? extends Interface> или C# where T : IInterface ограничения стиля, которые требуют, чтобы замещенный тип знал о (I)Interface,

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

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

Однако, если вы делаете что-то вроде Оптимизации пустого базового класса, вы можете минимизировать это:

структура А
{
    void func1() = 0;
};

структура B: A
{
    void func2() = 0;
};

структура C: B
{
    Int I;
};

Размер C будет два слова.

Кстати, MSVC 2008 имеет ключевое слово __interface.

A Visual C++ interface can be defined as follows: 

 - Can inherit from zero or more base
   interfaces.
 - Cannot inherit from a base class.
 - Can only contain public, pure virtual
   methods.
 - Cannot contain constructors,
   destructors, or operators.
 - Cannot contain static methods.
 - Cannot contain data members;
   properties are allowed.

Эта функция является специфичной для Microsoft. Внимание: __interface не имеет виртуального деструктора, который требуется, если вы удаляете объекты по указателям на интерфейс.

Нет хорошего способа реализовать интерфейс так, как вы просите. Проблема с таким подходом, как полностью абстрактный базовый класс ISerializable, заключается в том, что C++ реализует множественное наследование. Учтите следующее:

class Base
{
};
class ISerializable
{
  public:
    virtual string toSerial() = 0;
    virtual void fromSerial(const string& s) = 0;
};

class Subclass : public Base, public ISerializable
{
};

void someFunc(fstream& out, const ISerializable& o)
{
    out << o.toSerial();
}

Ясно, что функция toSerial() предназначена для сериализации всех членов Подкласса, в том числе тех, которые она наследует от Базового класса. Проблема в том, что нет пути от ISerializable к базе. Вы можете увидеть это графически, если выполните следующее:

void fn(Base& b)
{
    cout << (void*)&b << endl;
}
void fn(ISerializable& i)
{
    cout << (void*)&i << endl;
}

void someFunc(Subclass& s)
{
    fn(s);
    fn(s);
}

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

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