Лучший способ объявить интерфейс в C ++ 11

Как все мы знаем, некоторые языки имеют понятие интерфейсов. Это Java:

public interface Testable {
void test();
}

Как я могу добиться этого в C ++ (или C ++ 11) наиболее компактным способом и с небольшим шумом кода? Я был бы признателен за решение, которое не требует отдельного определения (пусть заголовка будет достаточно). Это очень простой подход, который даже я считаю глючным 😉

class Testable {
public:
virtual void test() = 0;
protected:
Testable();
Testable(const Testable& that);
Testable& operator= (const Testable& that);
virtual ~Testable();
}

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

44

Решение

Как насчет:

class Testable
{
public:
virtual ~Testable() { }
virtual void test() = 0;
}

В C ++ это не влияет на копируемость дочерних классов. Все это говорит о том, что ребенок должен осуществить test (что именно то, что вы хотите для интерфейса). Вы не можете создать экземпляр этого класса, поэтому вам не нужно беспокоиться о каких-либо неявных конструкторах, так как они никогда не могут быть вызваны напрямую как родительский тип интерфейса.

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

Также обратите внимание, что если вам не нужно полиморфное уничтожение, вы можете сделать свой деструктор защищенным, а не виртуальным.

37

Другие решения

Для динамического (динамического) полиморфизма я бы рекомендовал использовать Non-Virtual-интерфейс (NVI) идиома. Этот шаблон сохраняет интерфейс не виртуальным и общедоступным, деструктор виртуальным и общедоступным, а реализацию — чисто виртуальной и частной

class DynamicInterface
{
public:
// non-virtual interface
void fun() { do_fun(); } // equivalent to "this->do_fun()"
// enable deletion of a Derived* through a Base*
virtual ~DynamicInterface() = default;
private:
// pure virtual implementation
virtual void do_fun() = 0;
};

class DynamicImplementation
:
public DynamicInterface
{
private:
virtual void do_fun() { /* implementation here */ }
};

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

Для статического (полиморфизм во время компиляции), я бы рекомендовал использовать Любопытно повторяющийся шаблон (CRTP). Это значительно сложнее, потому что автоматическое приведение от базового к производному динамического полиморфизма должно выполняться с static_cast, Это статическое приведение может быть определено в вспомогательном классе, который происходит от каждого статического интерфейса.

template<typename Derived>
class enable_down_cast
{
private:
typedef enable_down_cast Base;
public:
Derived const* self() const
{
// casting "down" the inheritance hierarchy
return static_cast<Derived const*>(this);
}

Derived* self()
{
return static_cast<Derived*>(this);
}
protected:
// disable deletion of Derived* through Base*
// enable deletion of Base* through Derived*
~enable_down_cast() = default; // C++11 only, use ~enable_down_cast() {} in C++98
};

Затем вы определяете статический интерфейс следующим образом:

template<typename Impl>
class StaticInterface
:
// enable static polymorphism
public enable_down_cast< Impl >
{
private:
// dependent name now in scope
using enable_down_cast< Impl >::self;
public:
// interface
void fun() { self()->do_fun(); }
protected:
// disable deletion of Derived* through Base*
// enable deletion of Base* through Derived*
~StaticInterface() = default; // C++11 only, use ~IFooInterface() {} in C++98/03
};

и, наконец, вы делаете реализацию, которая происходит от интерфейса с сам как параметр

class StaticImplementation
:
public StaticInterface< StaticImplementation >
{
private:
// implementation
friend class StaticInterface< StaticImplementation > ;
void do_fun() { /* your implementation here */ }
};

Это все еще позволяет вам иметь несколько реализаций одного и того же интерфейса, но вам нужно знать во время компиляции, какую реализацию вы вызываете.

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

НОТА: для полиморфизма во время компиляции вы также можете использовать параметры шаблона. Разница между статическим интерфейсом через идиому CRTP и обычными параметрами шаблона заключается в том, что интерфейс типа CRTP является явным (основанным на функциях-членах), а интерфейс шаблона является неявным (основанным на допустимых выражениях).

39

Согласно Скотту Мейерсу (Effective Modern C ++): при объявлении интерфейса (или полиморфного базового класса) вам нужен виртуальный деструктор для правильных результатов таких операций, как delete или же typeid на объекте производного класса, доступ к которому осуществляется через указатель или ссылку на базовый класс.

virtual ~Testable() = default;

Однако объявленный пользователем деструктор подавляет генерацию
операции перемещения, поэтому для поддержки операций перемещения необходимо добавить:

Testable(Testable&&) = default;
Testable& operator=(Testable&&) = default;

Объявление операций перемещения отключает операции копирования, и вам также необходимо:

Testable(const Testable&) = default;
Testable& operator=(const Testable&) = default;

И конечный результат:

class Testable
{
public:
virtual ~Testable() = default; // make dtor virtual
Testable(Testable&&) = default;  // support moving
Testable& operator=(Testable&&) = default;
Testable(const Testable&) = default; // support copying
Testable& operator=(const Testable&) = default;

virtual void test() = 0;

};
17

Заменяя слово class с structвсе методы будут общедоступны по умолчанию, и вы можете сохранить строку.

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

Вы правы, что беспокоитесь о = оператор, так как сгенерированный компилятором, безусловно, будет делать неправильно. На практике никто никогда не беспокоится об этом, потому что копирование одного объекта интерфейса в другой никогда не имеет смысла; это не ошибка, которая случается обычно.

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

Окончательный результат на одну строку длиннее эквивалента Java:

struct Testable {
virtual void test() = 0;
virtual ~Testable();
};
11

Имейте в виду, что «правило трех» не нужно, если вы не управляете указателями, дескрипторами и / или у всех членов-данных класса есть свои собственные деструкторы, которые будут управлять любой очисткой. Также в случае виртуального базового класса, поскольку базовый класс никогда не может быть напрямую создан, нет необходимости объявлять конструктор, если все, что вы хотите сделать, это определить интерфейс, который не имеет членов данных … компилятор по умолчанию просто отлично. Единственный предмет, который вам нужно сохранить — это виртуальный деструктор, если вы планируете звонить delete на указатель типа интерфейса. Так что на самом деле ваш интерфейс может быть таким простым:

class Testable
{
public:
virtual void test() = 0;
virtual ~Testable();
}
7
По вопросам рекламы ammmcru@yandex.ru
Adblock
detector