Лучший способ объявить интерфейс на С++ 11
Как мы все знаем, некоторые языки имеют понятие интерфейсов. Это Java:
public interface Testable {
void test();
}
Как я могу достичь этого на С++ (или С++ 11) самым компактным способом и с небольшим шумом кода? Я был бы признателен за решение, для которого не требуется отдельное определение (пусть заголовок будет достаточным). Это очень простой подход, который даже я нахожу багги;-)
class Testable {
public:
virtual void test() = 0;
protected:
Testable();
Testable(const Testable& that);
Testable& operator= (const Testable& that);
virtual ~Testable();
}
Это только начало.. и уже дольше, что я бы хотел. Как его улучшить? Возможно, есть базовый класс где-то в пространстве имен std, сделанном именно для этого?
Ответы
Ответ 1
Как насчет:
class Testable
{
public:
virtual ~Testable() { }
virtual void test() = 0;
}
В С++ это не влияет на возможность перезаписи дочерних классов. Все это говорит о том, что ребенок должен реализовать test
(именно это вы хотите для интерфейса). Вы не можете создать экземпляр этого класса, поэтому вам не нужно беспокоиться о каких-либо неявных конструкторах, поскольку они никогда не могут быть вызваны непосредственно в качестве типа родительского интерфейса.
Если вы хотите, чтобы эти дочерние классы реализовали деструктор, вы также можете сделать это чистым (но вы все равно должны реализовать его в интерфейсе).
Также обратите внимание, что если вам не требуется полиморфное уничтожение, вы можете вместо этого сделать свой деструктор защищенным не виртуальным.
Ответ 2
Для динамического (runtime) полиморфизма я бы рекомендовал использовать идиому не виртуального интерфейса (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
от статического базового типа до динамического производного типа и вызовет соответствующую реализацию (обычно это происходит через таблицы с указателями на виртуальные функции).
Для статического (полиморфизм времени компиляции) я бы рекомендовал использовать шаблон Curiously Recurring Template Pattern (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 */ }
};
Это все еще позволяет вам иметь несколько реализаций одного и того же интерфейса, но вам нужно знать во время компиляции, какую реализацию вы вызываете.
Итак, когда использовать какую-либо форму? Обе формы позволят вам повторно использовать общий интерфейс и внедрять тестовое тестирование pre/post внутри класса интерфейса. Преимущество динамического полиморфизма заключается в том, что вы обладаете гибкостью во время выполнения, но платите за это в вызовах виртуальных функций (как правило, вызов через указатель функции, с небольшой возможностью для вложения). Статический полиморфизм - это зеркало: нет накладных расходов на виртуальные функции, но недостатком является то, что вам нужен более шаблонный код, и вам нужно знать, что вы вызываете во время компиляции. В основном, эффективность/гибкость компромисса.
ПРИМЕЧАНИЕ. для полиморфизма времени компиляции, вы также можете использовать параметры шаблона. Разница между статическим интерфейсом через идиому CRTP и обычными параметрами шаблона заключается в том, что интерфейс типа CRTP является явным (на основе функций-членов), а интерфейс шаблона неявный (на основе действительных выражений)
Ответ 3
Согласно Скотту Мейерсу (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;
};
Еще одна интересная статья здесь: Правило нуля в C++
Ответ 4
Заменив слово class
на struct
, все методы будут общедоступными по умолчанию, и вы можете сохранить строку.
Нет необходимости защищать конструктор, так как вы все равно не можете создавать экземпляр класса с помощью виртуальных методов. Это касается и конструктора копирования. Созданный компилятором конструктор по умолчанию будет пустым, поскольку у вас нет каких-либо элементов данных, и он полностью достаточен для ваших производных классов.
Вы правы, если вас беспокоит оператор =
, поскольку созданный компилятором, безусловно, сделает неправильную вещь. На практике никто никогда не беспокоится об этом, потому что копирование одного объекта интерфейса в другой никогда не имеет смысла; это не ошибка, которая обычно случается.
Деструкторы для наследуемого класса должны быть всегда публичными и виртуальными, или защищенными и не виртуальными. Я предпочитаю публичный и виртуальный в этом случае.
Конечный результат - только одна строка длиннее, чем эквивалент Java:
struct Testable {
virtual void test() = 0;
virtual ~Testable();
};
Ответ 5
Имейте в виду, что "правило из трех" не является необходимым, если вы не управляете указателями, ручками и/или все члены данных класса имеют свои собственные деструкторы, которые будут управлять любой очисткой. Также в случае виртуального базового класса, поскольку базовый класс никогда не может быть напрямую создан, нет необходимости объявлять конструктор, если все, что вы хотите сделать, - это определить интерфейс, который не имеет элементов данных... компилятор значения по умолчанию просто прекрасны. Единственный элемент, который вам нужно сохранить, - это виртуальный деструктор, если вы планируете вызывать delete
по указателю типа интерфейса. Таким образом, на самом деле ваш интерфейс может быть таким же простым, как:
class Testable
{
public:
virtual void test() = 0;
virtual ~Testable();
}