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

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

Ответ 1

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

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

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

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

Ответ 2

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

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

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

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

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

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

Ответ 3

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

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

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

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

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

Ответ 4

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

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

В соглашениях об именах... нет настоящих соглашений об именах, определенных на языке С++. Поэтому выберите его в своей среде.

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

Ответ 5

В С++ мы можем пойти дальше, чем простые безрисковые интерфейсы 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
{
    ...
};

Ответ 6

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

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

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

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

struct C: B
{
    int i;
};

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

Ответ 7

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

Ответ 8

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

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

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

Ответ 9

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

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 к Base. Вы можете увидеть это графически, если выполните следующее:

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