c++ 接口必须遵守五法则吗?
Posted
技术标签:
【中文标题】c++ 接口必须遵守五法则吗?【英文标题】:Must a c++ interface obey the rule of five? 【发布时间】:2018-10-02 08:40:44 【问题描述】:在定义接口类时,声明实例化方法的正确方法是什么?
出于显而易见的原因,抽象基类必须具有虚拟析构函数。但是,随后会给出以下编译警告:“'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:什么是满足接口类“五规则”的正确方法,因为用户必须定义一个虚拟析构函数。
感谢您的时间和帮助!
【问题讨论】:
“然后给出以下编译警告” - 由哪个编译器/版本? clang 6.0 警告一个折旧的复制构造函数。 clang-tidy 6.0 静态分析器给出了上面的特定警告字符串。 g++ 4.2.1 似乎没有针对这种情况触发警告。我正在使用 mac os High Sierra 10.13.4 基类析构函数应该被保护(防止多态删除)或公共和虚拟(使多态删除安全)。您拥有的受保护和虚拟组合很奇怪。 感谢您的提示。我将更新示例以反映这一点。 非拥有接口,不拥有,不应该拥有,这是零规则。 en.cppreference.com/w/cpp/language/rule_of_three 【参考方案1】:这是正确的吗?这些方法应该改为 = 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 A1;
ImplementationClass B2;
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
-
复制/移动特殊成员函数公开
使用复制/移动特殊成员函数作为默认和公共,(或不定义复制/移动成员函数),正常复制可以工作:
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© 特殊成员函数,则与复制相关的行为类似于 2:编译器将隐式生成已弃用的复制特殊成员(导致多态切片)。但是在这种情况下,编译器不会隐式生成移动特殊成员,因此将在可以移动的地方使用复制。
-
受保护的复制/移动成员函数(您的提议)
使用特殊的复制/移动成员函数作为默认和保护,如您的示例所示,您将防止多态复制,否则会导致切片:
*B_p = *A_p; // will not compile, copy is protected in InterfaceClass
但是,编译器会为 InterfaceClass 显式生成一个默认的复制赋值运算符,而 ImplementationClass 将能够隐式生成它的复制赋值运算符:
B = A; //will compile and work correctly
所以你的方法似乎是最好和最安全的选择
【讨论】:
【参考方案2】:对于析构函数,如果你想让它既是纯虚拟的又是默认的,你可以在实现中默认它:
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:视情况而定,但很可能您最好将它们保留为默认值。
【讨论】:
【参考方案3】:一般来说,如果 3 大特殊功能中的任何一个具有无 [trivial/default] 定义,则应定义其他 2 个。如果 2 个特殊移动函数没有 [trivial-default] 定义,那么您需要处理所有 5 个。 对于具有 nop 定义的 dtor 的接口,您无需费心定义其余部分 - 除非出于其他原因。 即使是非平凡的定义也不要求重新定义其他功能;只有当涉及到某种资源管理(例如内存、文件、io、同步...)时,才需要定义大 3(5)。
【讨论】:
只定义大3确实是安全的,但是移动语义特殊成员will not be implicitly generated,你会在对象可以移动的时候强制复制。 nop defined dtor 是什么意思?非公开的?这是否解决了多态副本中的切片问题?如果我确定它是安全的,我会很乐意不用在接口中定义特殊的成员函数 @Gianni nop 表示无操作。移动并不总是与复制不同或更便宜。移动只是对不可复制对象的优化或所有权转移的一种方式。以上是关于c++ 接口必须遵守五法则吗?的主要内容,如果未能解决你的问题,请参考以下文章