三/五规则适用于继承和虚拟析构函数吗?

Gra*_*pes 14 c++ c++11

我们假设我们有一个非常基本的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;  
};
Run Code Online (Sandbox Code Playgroud)

我们希望与扩展此类,class B所以我们增加我们的虚拟析构函数,改变一个成员virtual,改变privateprotected对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());
        }
};
Run Code Online (Sandbox Code Playgroud)

既然我们在class A do中添加了析构函数,我们需要创建一个复制构造函数和复制操作符吗?

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;

};
Run Code Online (Sandbox Code Playgroud)

对我来说这似乎是不必要的,因为默认的复制操作符和复制构造函数会做同样的事情.

Rei*_*ica 18

为了准备将来可能的语言演变,您应该在添加虚拟析构函数时显式默认复制/移动构造函数和赋值运算符.这是因为当类具有用户声明的析构函数时,C++ 11,12.8/7会隐式生成复制构造函数.

幸运的是,C++ 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;

};
Run Code Online (Sandbox Code Playgroud)

  • FWIW,你也可以`= default`虚拟析构函数. (4认同)
  • @MatthiasB是:标准说"user-declared"和`virtual~A()= default`是一个声明. (4认同)
  • `virtual ~A() = default` 析构函数是否仍算作用户生成? (2认同)

R. *_*des 12

三规则适用于一切.

如果您的类旨在用作多态基础,那么您很可能不希望使用其复制构造函数,因为它会切片.所以你必须做出决定.这就是三个规则的含义:你不能选择有一个析构函数而不考虑复制特殊成员.

请注意,三条规则并未说明您应该实现复制构造函数和复制赋值运算符.你应该以某种方式处理它们,因为如果你有自己的析构函数(它切片!),默认生成的那个很可能不合适,但你处理它们的方式不一定要实现它们.

你可能应该禁止它,因为使用多态基和价值语义往往像水和油混合.

我猜你可能会让它受到保护,所以派生类可以为自己的副本调用它,尽管我仍然认为这是一个值得怀疑的选择.

此外,从C++ 11开始,当用户声明析构函数时,不推荐使用复制特殊成员的生成.这意味着,如果您希望代码是向前兼容的,即使您需要默认的复制构造函数行为(一个可疑的选择),您也希望将其明确化.你可以用= default它.

  • 这比接受的答案更好。如果不提及切片,这里的回答就不完整。此外,“多态基础和值语义往往像水和油一样混合”是一个非常好的观察结果,如果您(像我一样)正在搜索此问答,则值得考虑。我认为古怪的类型擦除可能会产生异常,但这与常见情况相去甚远。 (2认同)

Mik*_*our 5

如果析构函数不执行任何操作,则(通常)复制/移动操作不需要执行除默认操作以外的任何操作。肯定没有必要编写执行默认值的版本,只是为了满足规则的过度简化。这样做只会增加代码的复杂性和错误的范围。

但是,如果您确实声明了一个虚拟析构函数,表明该类旨在成为一个多态基类,您可能会考虑删除复制/移动操作以防止切片。

本文为该规则提供了有用的措辞,包括

如果一个类有一个非空的析构函数,它几乎总是需要一个复制构造函数和一个赋值运算符。