提问人:user7119460 提问时间:4/22/2018 最后编辑:user7119460 更新时间:3/29/2021 访问量:4978
c++ 接口必须遵守五法则吗?
Must a c++ interface obey the rule of five?
问:
定义接口类时声明实例化方法的正确方法是什么?
出于显而易见的原因,抽象基类需要具有虚拟析构函数。但是,随后会给出以下编译警告:“'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;
氤;dr:满足接口类的“五法则”的正确方法是什么,因为用户必须定义一个虚拟析构函数。
感谢您的时间和帮助!
答:
对于析构函数,如果你想让它既是纯虚拟的又是默认的,你可以在实现中默认它:
class InterfaceClass
{
// -- Destructors --
virtual ~InterfaceClass() = 0;
};
InterfaceClass::~InterfaceClass() = default;
但是,如果析构函数是默认值还是空值,则没有太大区别。
现在是你剩下的问题。
通常,应默认为 copy 构造函数和赋值运算符。这样,它们就不会阻止在派生类中创建默认赋值运算符和复制构造函数。默认实现是正确的,因为没有要复制的不变性。
因此,如果您想实现 easy 方法,删除复制构造函数会损害: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
}
};
另请注意,复制/移动构造函数的默认实现不会意外地违背意图 - 因为无法创建抽象基类的实例。因此,您将始终复制派生类,并且它们应该定义复制是否合法。
但是,对于某些类来说,完全复制是没有意义的,在这种情况下,禁止在基类中复制/赋值可能是明智的。
氤;DR:这要看情况,但很可能你最好把它们保留为默认值。
这是正确的吗?这些方法应该=删除吗?
您的代码似乎是正确的。当您尝试以多态方式复制派生类时,将特殊复制/移动成员函数定义为默认和受保护函数的必要性是显而易见的。请考虑以下附加代码:
#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 个选项。
- 复制/移动成员函数 = 删除
在 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
- 复制/移动特殊成员函数 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.
- 未定义复制/移动特殊成员函数
如果未定义 move© 特殊成员函数,则与 copy 相关的行为类似于 2:编译器将隐式生成已弃用的 copy 特殊成员(导致多态切片)。但是,在这种情况下,编译器不会隐式生成移动特殊成员,因此在可以移动的地方将使用 copy。
- 受保护的复制/移动成员函数(您的建议)
如示例中所示,使用特殊的复制/移动成员函数作为默认和受保护功能,您将防止多态复制,否则会导致切片:
*B_p = *A_p; // will not compile, copy is protected in InterfaceClass
但是,编译器将为 InterfaceClass 显式生成默认的复制赋值运算符,并且 ImplementationClass 将能够隐式生成其复制赋值运算符:
B = A; //will compile and work correctly
因此,您的方法似乎是最好和最安全的选择
评论
On the other hand, you would also prevent normal copy, as the compiler won't be able to implicitly generate a copy assignment operator without the base class copy assignment operator (...)
一般来说,如果 3 大特殊函数中的任何一个没有 [trivial/default] 定义,则应定义其他 2 个。如果 2 个特殊移动函数没有 [trivial-default] 定义,则需要处理所有 5 个。 对于具有 nop 定义的 dtor 的接口,您无需费心定义其余部分 - 除非出于其他原因。 即使是微不足道的定义也无法重新定义其他功能;只有当涉及某种资源管理(例如内存、文件、IO、同步等)时,才需要定义 Big 3(5)。
评论
上一个:c++ 接口必须遵守五法则吗?
下一个:分配内存 - 指针
评论