5的规则-是否使用?


20

的3中的规则5规则状态在新的C ++标准):

如果您需要自己显式声明析构函数,复制构造函数或复制赋值运算符,则可能需要显式声明这三个函数。

但是,另一方面,马丁的“ 清理代码 ”建议删除所有空的构造函数和析构函数(第293页,G12:Clutter):

没有实现的默认构造函数有什么用?它要做的只是用毫无意义的工件使代码混乱。

那么,如何处理这两种相反的意见呢?是否应该真正实现空的构造函数/析构函数?


下一个示例准确地说明了我的意思:

#include <iostream>
#include <memory>

struct A
{
    A( const int value ) : v( new int( value ) ) {}
    ~A(){}
    A( const A & other ) : v( new int( *other.v ) ) {}
    A& operator=( const A & other )
    {
        v.reset( new int( *other.v ) );
        return *this;
    }

    std::auto_ptr< int > v;
};
int main()
{
    const A a( 55 );
    std::cout<< "a value = " << *a.v << std::endl;
    A b(a);
    std::cout<< "b value = " << *b.v << std::endl;
    const A c(11);
    std::cout<< "c value = " << *c.v << std::endl;
    b = c;
    std::cout<< "b new value = " << *b.v << std::endl;
}

使用g ++ 4.6.1与以下命令进行编译:

g++ -std=c++0x -Wall -Wextra -pedantic example.cpp

的析构函数为struct A空,并非真正需要。那么,它应该在那里还是应该删除?


15
这两个引号讨论了不同的事情。或者我完全想念你的意思。
本杰明·班尼尔

1
@honk在我团队的编码标准中,我们有一条规则始终声明所有4个(构造函数,析构函数,复制构造函数)。我想知道这样做是否真的有意义。我是否真的必须始终声明析构函数,即使它们为空?
BЈовић

至于空的析构函数,请考虑一下:codesynthesis.com/~boris/blog/2012/04/04/…。否则的3(5)的规则使我感觉良好,不知道为什么一个想4的规则
本杰明Bannier

@honk提防您在网上找到的信息。并非所有情况都是如此。例如,virtual ~base () = default;不编译(有一个很好的理由)
BЈовић

@VJovic,不,您不必声明一个空的析构函数,除非您需要将其虚拟化。而且,当我们在讨论这个主题时,您都不应该使用auto_ptr其中任何一个。
Dima 2012年

Answers:


44

首先,规则说“可能”,因此并不总是适用。

我在这里看到的第二点是,如果必须声明三者之一,那是因为它在做一些特殊的事情,例如分配内存。在这种情况下,其他对象将不会为空,因为它们必须处理相同的任务(例如,在复制构造函数中复制动态分配的内存的内容或释放此类内存)。

因此,结论是,您不应声明空的构造函数或析构函数,但是很有可能,如果需要一个,则也需要其他的。

如您的示例:在这种情况下,可以不使用析构函数。显然它什么也没做。使用智能指针是3规则不成立的地方和原因的完美示例。

如果您可能忘记实现以前可能会错过的重要功能,那么它只是指导您重新查看代码的指南。


使用智能指针,析构函数在大多数情况下为空(我想说代码库中有超过99%的析构函数为空,因为几乎每个类都使用pimpl习惯用语)。
BЈовић

哇,真叫人讨厌,我叫它臭。对于许多编译器而言,pimpled将更难优化(例如,难以内联)。
本杰明·班尼尔

@honk“许多已编译的编译器”是什么意思?:)
BЈовић

@VJovic:对不起,错字:“ pimpled code”
Benjamin Bannier 2012年

4

这里确实没有矛盾。3规则讨论析构函数,复制构造函数和复制赋值运算符。Bob叔叔谈论空的默认构造函数。

如果需要析构函数,则您的类可能包含指向动态分配的内存的指针,并且您可能想要一个复制ctor和一个operator=()进行深层复制的。这与是否需要默认构造函数完全正交。

还要注意,在C ++中,有些情况下确实需要默认构造函数,即使它为空。假设您的类具有非默认构造函数。在这种情况下,编译器将不会为您生成默认的构造函数。这意味着此类的对象无法存储在STL容器中,因为这些容器希望这些对象是默认可构造的。

另一方面,如果您不打算将类的对象放到STL容器中,那么空的默认构造函数无疑是毫无用处的。


2

在这里,与默认的一个构造函数/赋值/析构函数等效的潜能(*)具有一个目的:记录有关该问题的事实,并确定默认行为是正确的。顺便说一句,在C ++ 11中,事情还不够稳定,无法知道是否=default可以达到这个目的。

(还有另一个潜在的目的:提供一个异常定义而不是默认的内联定义,如果有任何理由,最好进行明确记录)。

(*)有潜力,因为我不记得一个现实生活中的案例,其中三个规则都不适用,如果我必须在一人中做某事,那么我就必须在其他人中做某事。


添加示例后进行编辑。您使用auto_ptr的示例很有趣。您使用的是智能指针,但没有一个能胜任工作。我宁愿写一本(特别是在这种情况经常发生的情况下),而不是照做。(如果我没记错的话,标准和提升都不提供)。


这个例子说明了我的观点。析构函数并不是真正需要的,但是3的规则告诉它应该在那里。
BЈовић

1

5规则是3规则的谨慎扩展,3规则是一种谨慎的行为,可能会导致对象滥用。

如果您需要一个析构函数,则意味着您进行了一些除默认之外的“资源管理”(仅构造和分解)。

由于复制,赋值,移动和转移默认情况下是复制,如果您不仅仅持有value,则必须定义要执行的操作。

就是说,如果定义了移动,C ++将删除该副本,如果定义了副本,则C ++将删除该副本。在大多数情况下,您必须定义是否要模拟值(因此,复制mut克隆资源,而移动没有意义)或资源管理器(因此,将资源移动,而复制没有意义):规则3的规则成为其他3的规则)

必须同时定义复制和移动(5条规则)的情况非常少见:通常,您具有“大价值”,如果将其赋予不同的对象,则必须将其复制,但是如果从临时对象获取则可以移动(避免)一个克隆然后销毁)。STL容器或算术容器就是这种情况。

的情况可以是矩阵:他们必须支持拷贝,因为它们值(a=b; c=b; a*=2; b*=3;一定不能相互影响),但是它们可以通过支持也移动(被优化a = 3*b+4*c具有+带两个临时变量,并产生临时:避免克隆和删除可有用)


1

我更喜欢对规则3的另一种表述,这似乎更合理,即“如果您的类需要一个析构函数(而不是一个空的虚拟析构函数),它可能还需要一个复制构造函数和赋值运算符”。

通过析构函数将其指定为单向关系可以使一些事情变得更清楚:

  1. 仅当您提供非默认复制构造函数或赋值运算符作为优化时,它才适用。

  2. 该规则的原因是,默认的复制构造函数或赋值运算符可能会破坏手动资源管理。如果您正在手动管理资源,则可能已经意识到您将需要析构函数来释放它们。


-3

讨论中还没有提到另一点:析构函数应该始终是虚拟的。

struct A
{
    A( const int value ) : v( new int( value ) ) {}
    virtual ~A(){}
    ...
}

需要在基类中将构造函数声明为虚拟的,以使其在所有派生类中也是虚拟的。因此,即使您的基类不需要析构函数,您最终也将声明并实现一个空的析构函数。

如果在(-Wall -Wextra -Weffc ++)上发出所有警告,则g ++会对此发出警告。我认为始终在任何类中声明虚拟析构函数是一种好习惯,因为您永远不会知道您的类是否最终将成为基类。如果不需要虚拟析构函数,则不会造成任何危害。如果是这样,则可以节省时间来查找错误。


1
但是我不想要虚拟构造函数。如果这样做,则对任何方法的每次调用都将使用虚拟调度。顺便说一句,请注意,在c ++中没有“虚拟构造函数”之类的东西。另外,我将该示例编译为非常高的警告级别。
2012年

IIRC是gcc用于警告的规则,也是我通常遵循的规则,即如果类中还有其他虚拟方法,则应该有一个虚拟析构函数。
Jules 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.