Как я могу симулировать интерфейсы в C ++? - PullRequest
37 голосов
/ 01 августа 2009

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

Ответы [ 9 ]

37 голосов
/ 01 августа 2009

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

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

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

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

11 голосов
/ 01 августа 2009

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

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

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

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

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

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

7 голосов
/ 01 августа 2009

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

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

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

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

Добавление нескольких базовых классов с помощью виртуальных функций-членов, вероятно, означает, что фактически вы получаете пустую оптимизацию базового класса только один раз, потому что в типичной реализации объекту потребуется несколько указателей 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
5 голосов
/ 01 августа 2009

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

3 голосов
/ 03 августа 2009

В C ++ мы можем пойти дальше простых интерфейсов Java & co без поведения. Мы можем добавить явные контракты (как в Дизайн по контракту ) с шаблоном 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
{
    ...
};
1 голос
/ 18 апреля 2019

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

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

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

1 голос
/ 03 августа 2009

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

1 голос
/ 01 августа 2009

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

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

struct A
{
    void func1() = 0;
};

struct B: A
{
    void func2() = 0;
};

struct C: B
{
    int i;
};

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

0 голосов
/ 11 июня 2014

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

...