Применяется ли правило трех/пяти к наследованию и виртуальным деструкторам?
Предположим, что у нас есть очень простой class A
:
class A {
public:
void SetName(const std::string& newName) {
m_name=newName;
}
void Print() const {
std::printf("A::Print(). Name: %s\n",m_name.c_str());
}
private:
std::string m_name;
};
Мы хотим расширить этот класс с помощью class B
, поэтому добавим наш виртуальный деструктор, изменим элемент на virtual
и изменим private
на protected
для inh:
class A {
public:
virtual ~A() {}
void SetName(const std::string& newName) {
m_name=newName;
}
virtual void Print() const {
std::printf("A::Print(). Name: %s\n",m_name.c_str());
}
protected:
std::string m_name;
};
class B : public A {
public:
virtual void Print() const {
std::printf("B::Print(). Name: %s\n",m_name.c_str());
}
};
Теперь, когда мы добавили деструктор в class A
, нам нужно создать конструктор копирования и скопировать оператор так:
class A {
public:
virtual ~A() {}
A() = default;
A(const A& copyFrom){
*this = copyFrom;
}
virtual A& operator=(const A& copyFrom){
m_name=copyFrom.m_name;
return *this;
};
void SetName(const std::string& newName) {
m_name=newName;
}
virtual void Print() const {
std::printf("A::Print(). Name: %s\n",m_name.c_str());
}
protected:
std::string m_name;
};
Мне это кажется ненужным, поскольку оператор копирования по умолчанию и конструктор копирования будут делать то же самое.
Ответы
Ответ 1
Чтобы быть готовым к будущей эволюции языка, вы должны явно указать конструкторы copy/move и операторы присваивания при добавлении виртуального деструктора. Это потому, что С++ 11, 12.8/7 делает неявное генерирование конструкторов копий, устаревших, когда класс имеет объявленный пользователем деструктор.
К счастью, явное умолчание С++ 11 упрощает их определение:
class A {
public:
virtual ~A() {}
A() = default;
A(const A& copyFrom) = default;
A& operator=(const A& copyFrom) = default;
A(A &&) = default;
A& operator=(A &&) = default;
void SetName(const std::string& newName) {
m_name=newName;
}
virtual void Print() const {
std::printf("A::Print(). Name: %s\n",m_name.c_str());
}
protected:
std::string m_name;
};
Ответ 2
Правило из трех применимо ко всему.
Если ваш класс предназначен для использования в качестве полиморфной базы, очень маловероятно, что вы захотите использовать его конструктор копирования, потому что он срезает. Поэтому вы должны принять решение. Это то, о чем правило три: вы не можете выбрать деструктор без учета специальных экземпляров копии.
Обратите внимание, что правило три не говорит, что вы должны реализовать конструктор копирования и оператор копирования. Вы должны иметь дело с ними каким-то образом, потому что генерируемый по умолчанию, скорее всего, не подходит, если у вас есть собственный деструктор (он срезает!), Но способ, которым вы имеете дело с ними, не должен их реализовывать.
Вероятно, вам следует просто запретить это, так как использование полиморфных оснований и семантики значений, как правило, смешиваются как вода и масло.
Я думаю, вы могли бы сделать его защищенным, так что производные классы могут вызвать его для своих собственных копий, хотя я все же считаю это сомнительным выбором.
Кроме того, поскольку С++ 11 генерация специальных экземпляров копирования устарела, когда деструктор объявляется пользователем. Это означает, что если вы хотите, чтобы ваш код был совместим с пересылкой, даже если вам требуется поведение конструктора по умолчанию (сомнительный выбор), вы захотите сделать это явным. Вы можете использовать = default
для этого.
Ответ 3
Если деструктор ничего не делает, тогда (как правило) нет необходимости в операциях копирования/перемещения делать что-либо, кроме значения по умолчанию. Конечно, нет необходимости писать версии, которые делают то, что были по умолчанию, просто для того, чтобы удовлетворить чрезмерное упрощение правила. Все, что делает это, - это увеличение сложности кода и объема ошибок.
Однако, если вы объявите виртуальный деструктор, указав, что класс предназначен для полиморфного базового класса, вы можете рассмотреть возможность удаления операций копирования/перемещения для предотвращения нарезки.
Эта статья дает полезную формулировку для правила, включая
Если класс имеет непустой деструктор, ему почти всегда нужен конструктор копирования и оператор присваивания.