19

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

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

感谢您的时间和帮助!

4

3 回答 3

7

这个对吗?这些方法应该改为 = 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;
}
   

并考虑在 InterfaceClass 中定义特殊复制/移动成员函数的 4 个选项。

  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. 复制/移动特殊成员函数 public

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

B = A; //will compile and work correctly

但将启用多态复制并导致切片:

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

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

  1. 受保护的复制/移动成员函数(您的提议)

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

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

但是,编译器将为 InterfaceClass 显式生成一个默认的复制赋值运算符,而 ImplementationClass 将能够隐式生成其复制赋值运算符:

B = A; //will compile and work correctly

所以你的方法似乎是最好和最安全的选择

于 2019-03-07T12:05:19.090 回答
1

对于析构函数,如果你想让它既是纯虚拟的又是默认的,你可以在实现中默认它:

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:这取决于,但很可能您最好将它们保留为默认值。

于 2019-02-23T18:43:36.527 回答
0

一般来说,如果 3 大特殊函数中的任何一个具有无 [trivial/default] 定义,则应定义其他 2 个。如果 2 个特殊移动函数没有 [trivial-default] 定义,那么您需要处理所有 5 个。对于具有 nop 定义的 dtor 的接口,您无需费心定义其余的 - 除非其他原因。即使是非平凡的定义也不要求重新定义其他功能;只有当涉及某种资源管理(例如内存、文件、io、同步...)时,才需要定义大 3(5)。

于 2019-03-07T16:25:03.953 回答