Применяется ли правило трех/пяти к наследованию и виртуальным деструкторам?

Предположим, что у нас есть очень простой 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

Если деструктор ничего не делает, тогда (как правило) нет необходимости в операциях копирования/перемещения делать что-либо, кроме значения по умолчанию. Конечно, нет необходимости писать версии, которые делают то, что были по умолчанию, просто для того, чтобы удовлетворить чрезмерное упрощение правила. Все, что делает это, - это увеличение сложности кода и объема ошибок.

Однако, если вы объявите виртуальный деструктор, указав, что класс предназначен для полиморфного базового класса, вы можете рассмотреть возможность удаления операций копирования/перемещения для предотвращения нарезки.

Эта статья дает полезную формулировку для правила, включая

Если класс имеет непустой деструктор, ему почти всегда нужен конструктор копирования и оператор присваивания.