Должен ли интерфейс C++ подчиняться правилу пяти?
Как правильно объявлять методы создания экземпляров при определении класса интерфейса?
Абстрактные базовые классы должны иметь виртуальный деструктор по очевидным причинам. Однако затем выдается следующее предупреждение компиляции: "InterfaceClass определяет деструктор не по умолчанию, но не определяет конструктор копирования, оператор назначения копирования, конструктор перемещения или оператор назначения перемещения", что является "правилом пяти". ".
Я понимаю, почему "правило пяти" следует соблюдать в целом, но применимо ли оно к абстрактному базовому классу или интерфейсу?
Моя импликация тогда:
class InterfaceClass
{
// == INSTANTIATION ==
protected:
// -- Constructors --
InterfaceClass() = default;
InterfaceClass(const InterfaceClass&) = default;
InterfaceClass(InterfaceClass&&) = default;
public:
// -- Destructors --
virtual ~InterfaceClass() = 0;
// == OPERATORS ==
protected:
// -- Assignment --
InterfaceClass& operator=(const InterfaceClass&) = default;
InterfaceClass& operator=(InterfaceClass&&) = default;
// == METHODS ==
public:
// Some pure interface methods here...
};
// == INSTANTIATION ==
// -- Destructors --
InterfaceClass::~InterfaceClass()
{
}
Это правильно? Должны ли эти методы быть = delete
вместо? Есть ли какой-нибудь способ объявить деструктор виртуальным чистым, оставаясь при этом по умолчанию?
Даже если я объявлю деструктор как: virtual ~InterfaceClass() = default;
Если я не буду явно задавать по умолчанию остальные четыре, я получу то же предупреждение компилятора.
Tl; dr: Как правильно выполнить "правило пяти" для класса интерфейса, поскольку пользователь должен определить виртуальный деструктор.
Спасибо за ваше время и помощь!
0 ответов
Это правильно? Должны ли эти методы быть = удалить вместо этого?
Ваш код кажется правильным. Необходимость определения специальных функций-членов копирования / перемещения по умолчанию и защищенных становится очевидной при попытке полиморфно скопировать производный класс. Рассмотрим этот дополнительный код:
#include <iostream>
class ImplementationClass : public InterfaceClass
{
private:
int data;
public:
ImplementationClass()
{
data=0;
};
ImplementationClass(int p_data)
{
data=p_data;
};
void print()
{
std::cout<<data<<std::endl;
};
};
int main()
{
ImplementationClass A{1};
ImplementationClass B{2};
InterfaceClass *A_p = &A;
InterfaceClass *B_p = &B;
// polymorphic copy
*B_p=*A_p;
B.print();
// regular copy
B=A;
B.print();
return 0;
}
И рассмотрите 4 варианта определения специальных функций-членов копирования / перемещения в вашем InterfaceClass.
- копировать / переместить функции-члены = удалить
С помощью специальных функций-членов копирования / перемещения, удаленных в вашем InterfaceClass, вы предотвратите полиморфное копирование:
*B_p = *A_p; // would not compile, copy is deleted in InterfaceClass
Это хорошо, потому что полиморфная копия не сможет копировать элемент данных в производном классе.
С другой стороны, вы бы также запретили нормальное копирование, поскольку компилятор не сможет неявно сгенерировать оператор присваивания копии без оператора присваивания копии базового класса:
B = A; // would not compile either, copy assignment is deleted in ImplementationClass
- копировать / перемещать специальные функции-члены
При использовании специальных функций-членов копирования / перемещения по умолчанию и общедоступных (или без определения функций-членов копирования / перемещения) будет работать обычное копирование:
B = A; //will copile and work correctly
но полиморфная копия будет включена и приведет к нарезке:
*B_p = *A_p; // will compile but not copy the extra data members in the derived class.
- копировать / перемещать специальные функции-члены не определены
Если специальные функции-члены перемещения и копирования не определены, поведение по отношению к копированию аналогично 2: компилятор неявно генерирует устаревшие специальные элементы-копии (что приводит к полиморфному разрезанию). Однако в этом случае компилятор не будет неявно генерировать специальные члены перемещения, поэтому копирование будет использоваться там, где перемещение было бы возможным.
- функции защищенного копирования / перемещения (ваше предложение)
С помощью специальных функций-членов копирования / перемещения по умолчанию и защищенных, как в вашем примере, вы предотвратите полиморфное копирование, которое может привести к нарезке:
*B_p = *A_p; // will not compile, copy is protected in InterfaceClass
Тем не менее, компиляция будет явно генерировать оператор назначения копирования по умолчанию InterfaceClass, и AvailabilityClass сможет неявно генерировать свой оператор назначения копирования:
B = A; //will compile and work correctly
Таким образом, ваш подход кажется лучшей и самой безопасной альтернативой
Для деструктора, если вы хотите сделать его как чисто виртуальным, так и по умолчанию, вы можете использовать его по умолчанию в реализации:
class InterfaceClass
{
// -- Destructors --
virtual ~InterfaceClass() = 0;
};
InterfaceClass::~InterfaceClass() = default;
Это не имеет большого значения, если деструктор по умолчанию или пустой.
Теперь для остальной части вашего вопроса.
Обычно у вас должен быть конструктор копирования и оператор присваивания по умолчанию. Таким образом, они не мешают создавать операторы присваивания по умолчанию и конструктор копирования в производных классах. Реализация по умолчанию верна, так как нет инварианта для копирования.
Так что если вы хотите легко реализовать Clone
Метод, удаляющий конструктор копирования, повредит:
class InterfaceClass
{
virtual InterfaceClass* Clone() = 0;
virtual ~InterfaceClass() = 0;
};
class ImplementationClass : public InterfaceClass
{
public:
// This will not work if base copy constructor is deleted
ImplementationClass(const ImplementationClass&) = default;
// Writing copy constructor manually may be cumbersome and hard to maintain,
// if class has a lot of members
virtual ImplementationClass* Clone() override
{
return new ImplementationClass(*this); // Calls copy constructor
}
};
Также обратите внимание, что реализация конструктора копирования / перемещения по умолчанию не будет случайно использоваться против намерения - поскольку экземпляры абстрактного базового класса не могут быть созданы. Таким образом, вы всегда будете копировать производные классы, и они должны определить, является ли копирование легальным или нет.
Однако для некоторых классов создание копий полностью не имеет смысла, в этом случае может быть целесообразно запретить копирование / присвоение в самом базовом классе.
Tl;dr: это зависит, но, скорее всего, вам лучше оставить их по умолчанию.
В общем, если какая-либо из трех больших специальных функций не имеет определения [trivial/default], следует определить другие 2. Если у 2 специальных функций перемещения нет определения [trivial-default], вам нужно позаботиться обо всех 5. В случае интерфейса с dop-определенным dtor вам не нужно беспокоиться об определении остальных - если только для других причины. Даже нетривиальные определения не требуют переопределения других функций; только когда требуется какое-то управление ресурсами (например, память, файл, io, синхронизация...), нужно определить большую 3(5).