c ++接口必须遵守五条规则吗?

时间:2018-04-22 01:49:12

标签: c++ abstract-class c++17 virtual-destructor rule-of-three

在定义接口类时声明实例化方法的正确方法是什么?

出于显而易见的原因,抽象基类需要具有虚拟析构函数。但是,然后给出以下编译警告:“'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:满足接口类的“五规则”的正确方法是什么,因为用户必须定义​​一个虚拟析构函数。

感谢您的时间和帮助!

3 个答案:

答案 0 :(得分:2)

  

这是正确的吗?这些方法应该改为= delete吗?

您的代码似乎正确。当您尝试多态复制派生类时,很明显需要将特殊的复制/移动成员函数定义为默认值和受保护。考虑以下附加代码:

#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中定义特殊的复制/移动成员函数的选项。

  1. 复制/移动成员函数= 删除

在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 
  1. 复制/移动特殊成员功能公开

使用复制/移动特殊成员函数作为默认成员和公共成员(或未定义复制/移动成员函数),普通复制将起作用:

B = A; //will copile and work correctly

但多态副本将被启用并导致切片:

*B_p = *A_p; // will compile but not copy the extra data members in the derived class. 
  1. 复制/移动未定义的特殊成员功能

如果未定义move&copy特殊成员函数,则复制的行为类似于2:编译器将隐式生成已弃用的copy特殊成员(导致多态切片)。但是,在这种情况下,编译器不会隐式生成移动特殊成员,因此将在可能进行移动的地方使用复制。

  1. 受保护的复制/移动成员功能(您的建议)

使用特殊的复制/移动成员功能作为默认值并受保护,如您的示例所示,您将防止多态复制可能导致切片:

*B_p = *A_p; // will not compile, copy is protected in InterfaceClass

但是,编译将显式生成默认的副本分配运算符InterfaceClass,而ImplementationClass将能够隐式生成其副本分配运算符:

B = A; //will compile and work correctly

因此,您的方法似乎是最好,最安全的选择

答案 1 :(得分:1)

对于析构函数,如果要将其设为纯虚拟和默认值,则可以在实现中将其设为默认值:

class InterfaceClass
{
    //  -- Destructors --
    virtual ~InterfaceClass() = 0;
};

InterfaceClass::~InterfaceClass() = default;

不过,如果析构函数为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
    }
};

还请注意,不会意外地使用copy / move构造函数的默认实现-因为无法创建抽象基类的实例。因此,您将始终在复制派生类,并且应定义复制是否合法。

但是,对于某些完全复制的类来说,这样做是没有意义的,在这种情况下,最好在最基本的类中禁止复制/分配。

Tl; dr:要视情况而定,但最有可能的是,最好将它们保留为默认值。

答案 2 :(得分:0)

通常,如果前3个特殊功能中的任何一个都不具有[普通/默认]定义,则应定义另外2个。如果这2个特殊的move函数没有none- [trivial-default]定义,那么您需要注意所有5个。 对于带有nop定义的dtor的接口,您无需费心定义其余部分-除非出于其他原因。 即使是非常重要的定义也不能确定其他功能的重新定义。仅当涉及某种资源管理(例如,内存,文件,io,同步...)时,才需要定义大3(5)。