Примечание: ниже приведен код C ++ 03, но мы ожидаем перехода на C ++ 11 в ближайшие два года, поэтому мы должны помнить об этом.
Я пишу руководство (для новичков, среди прочего) о том, как написать абстрактный интерфейс на C ++. Я прочитал обе статьи Саттера на эту тему, искал в Интернете примеры и ответы и провел несколько тестов.
Этот код не должен компилироваться!
void foo(SomeInterface & a, SomeInterface & b)
{
SomeInterface c ; // must not be default-constructible
SomeInterface d(a); // must not be copy-constructible
a = b ; // must not be assignable
}
Все приведенные выше поведения находят источник своей проблемы в нарезке : абстрактный интерфейс (или неконечный класс в иерархии) не должен быть ни конструируемым, ни копируемым / присваиваемым, даже если производный класс может быть.
0-е решение: базовый интерфейс
class VirtuallyDestructible
{
public :
virtual ~VirtuallyDestructible() {}
} ;
Это решение простое и несколько наивное: оно не отвечает всем нашим ограничениям: оно может быть построено по умолчанию, построено при копировании и назначено при копировании (я даже не уверен насчет конструкторов перемещения и присваивания, но у меня есть еще 2 года, чтобы понять это из).
- Мы не можем объявить деструктор чисто виртуальным, потому что нам нужно поддерживать его встроенным, и некоторые из наших компиляторов не будут переваривать чисто виртуальные методы со встроенным пустым телом.
- Да, единственная цель этого класса - сделать реализаторы практически разрушаемыми, что является редким случаем.
- Даже если бы у нас был дополнительный виртуальный чистый метод (что в большинстве случаев), этот класс все равно можно было бы назначать для копирования.
Так что нет ...
1-е решение: boost :: noncopyable
class VirtuallyDestructible : boost::noncopyable
{
public :
virtual ~VirtuallyDestructible() {}
} ;
Это лучшее решение, потому что оно простое, понятное и C ++ (без макросов)
Проблема в том, что он все еще не работает для этого конкретного интерфейса, потому что VirtuallyConstructible все еще может быть создан по умолчанию .
- Мы не можем объявить деструктор чистым виртуальным, потому что нам нужно поддерживать его встроенным, а некоторые наши компиляторы не будут его переваривать.
- Да, единственная цель этого класса - сделать реализаторы практически разрушаемыми, что является редким случаем.
Другая проблема заключается в том, что классы, реализующие не копируемый интерфейс, должны затем явно объявить / определить конструктор копирования и оператор присваивания, если им нужны эти методы (а в нашем коде у нас есть классы значений, к которым наш клиент может получить доступ через интерфейсы).
Это идет вразрез с правилом нуля, к которому мы хотим обратиться: если реализация по умолчанию в порядке, то мы должны быть в состоянии ее использовать.
2-е решение: сделать их защищенными!
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
// With C++11, these methods would be "= default"
MyInterface() {}
MyInterface(const MyInterface & ) {}
MyInterface & operator = (const MyInterface & ) { return *this ; }
} ;
Этот шаблон следует техническим ограничениям, которые у нас были (по крайней мере, в пользовательском коде): MyInterface не может быть создан по умолчанию, не может быть создан для копирования и не может быть назначен для копирования.
Кроме того, он не накладывает искусственных ограничений на реализацию классов , которые затем могут свободно следовать правилу нуля или даже объявлять несколько конструкторов / операторов как «= default» в C ++ 11/14 без проблем.
Теперь, это довольно многословно, и альтернативой будет использование макроса, что-то вроде:
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
DECLARE_AS_NON_SLICEABLE(MyInterface) ;
} ;
Защищенный должен оставаться вне макроса (потому что у него нет области видимости).
Правильно «пространство имен» (то есть с префиксом названия вашей компании или продукта), макрос должен быть безвредным.
Преимущество заключается в том, что код размещается в одном источнике, а не копируется во все интерфейсы. Если бы в будущем явным образом отключить конструктор перемещения и назначение перемещения, это было бы очень легким изменением в коде.
Вывод
- Я параноик, чтобы хотеть, чтобы код был защищен от нарезки в интерфейсах? (Я верю, что нет, но никто не знает ...)
- Что является лучшим решением среди вышеперечисленных?
- Есть ли другое, лучшее решение?
Пожалуйста, помните, что это шаблон, который будет служить руководством для новичков (среди прочих), поэтому решение типа: «У каждого случая должна быть своя реализация» не является жизнеспособным решением.
Щедрость и результаты
Я присудил награду coredump из-за времени, потраченного на ответы на вопросы, и актуальности ответов.
Мое решение проблемы, вероятно, пойдет примерно так:
class MyInterface
{
DECLARE_CLASS_AS_INTERFACE(MyInterface) ;
public :
// the virtual methods
} ;
... со следующим макросом:
#define DECLARE_CLASS_AS_INTERFACE(ClassName) \
public : \
virtual ~ClassName() {} \
protected : \
ClassName() {} \
ClassName(const ClassName & ) {} \
ClassName & operator = (const ClassName & ) { return *this ; } \
private :
Это жизнеспособное решение для моей проблемы по следующим причинам:
- Этот класс не может быть создан (конструкторы защищены)
- Этот класс может быть практически уничтожен
- Этот класс может быть унаследован без наложения чрезмерных ограничений на наследуемые классы (например, наследующий класс может быть по умолчанию копируемым)
- Использование макроса означает, что «объявление» интерфейса легко узнаваемо (и доступно для поиска), а его код размещен в одном месте, что облегчает его изменение (имя с соответствующим префиксом удалит нежелательные конфликты имен)
Обратите внимание, что другие ответы дали ценную информацию. Спасибо всем, кто дал ему шанс.
Обратите внимание, что я думаю, что я все еще могу назначить еще одну награду за этот вопрос, и я оцениваю достаточно много просвещающих ответов, чтобы, если я увижу один, я открыл бы награду только для того, чтобы назначить ее этому ответу.
virtual ~VirtuallyDestructible() = 0
виртуальное наследование интерфейсных классов (только с абстрактными членами). Вы можете опустить это VirtuallyDestructible, скорее всего.
virtual void bar() = 0;
например? Это предотвратит создание вашего интерфейса.