10

我想知道是否有任何理由明确编写与 C++ 的默认行为相同的代码。

这是一些代码:

class BaseClass
{
public:
    virtual ~BaseClass() {}

    virtual void f() { /* do something */ }
};

class ExplicitClass
    : public BaseClass
{
public:
    ExplicitClass()
        : BaseClass()   // <-- explicit call of base class constructor
    {
        // empty function
    }

    virtual ~ExplicitClass() {}  // <-- explicit empty virtual destructor

    virtual void f() { BaseClass::f(); }  // <-- function just calls base
};

class ImplicitClass
    : public BaseClass
{    
};

我主要对重构和不断变化的代码库感到好奇。我不认为许多编码人员打算编写这样的代码,但是当代码随着时间的推移发生变化时,它最终会看起来像这样。

将代码保留在 中是否有任何意义ExplicitClass?我可以看到它向您展示正在发生的事情的好处,但它给人的印象是容易掉毛且有风险。

就我个人而言,我更喜欢删除任何默认行为代码(如ImplicitClass)的代码。

是否有任何赞成一种方式的共识?

4

4 回答 4

1

这个问题有两种方法:

  1. 定义一切,即使编译器生成相同,
  2. 没有定义任何编译会做得更好的东西。

(1) 的信徒正在使用如下规则:“始终定义默认 c-tor、复制 C-tor、赋值运算符和 d-tor”。

(1) 信徒认为拥有比错过更安全。不幸的是(1)特别受到我们的经理们的喜爱——他们相信拥有总比没有好。Sp 这样的规则“总是定义四大”进入“编码标准”,必须遵守。

我相信(2)。对于存在此类编码标准的公司,我总是发表评论“不要定义复制 c-tor,因为编译器做得更好”

于 2012-09-21T00:22:14.633 回答
0

无论哪种方式都可以,只要您了解那里真正发生了什么,以及不自己编写函数可能出现的问题。

例外安全

编译器将生成隐式添加必要throw条件的函数。对于隐式创建的构造函数,这将是基类和成员的每个抛出条件。

格式错误的代码

在一些棘手的情况下,一些自动生成的成员函数格式不正确。这是一个例子:

class Derived;

class Base
{
public:
  virtual Base& /* or Derived& */
  operator=( const Derived& ) throw( B1 );

  virtual ~Base() throw( B2 );
};

class Member
{
public:
  Member& operator=( const Member& ) throw( M1 );
  ~Member() throw( M2 );
};

class Derived : public Base
{
  Member m_;
  //   Derived& Derived::operator=( const Derived& )
  //            throw( B1, M1 ); // error, ill-formed
  //   Derived::~Derived()
  //            throw( B2, M2 ); // error, ill-formed
};

格式operator=错误是因为它的 throw 指令至少应该像它的基类一样具有限制性,这意味着它应该抛出 B1,或者根本不抛出任何东西。这是有道理的,因为 Derived 对象也可以被视为 Base 对象。

请注意,只要您从不调用它,拥有一个格式错误的函数是完全合法的。

我基本上是在这里重写 GotW #69,所以如果你想了解更多细节,你可以在这里找到它们

于 2012-09-21T01:10:47.227 回答
0

这取决于您喜欢如何构建和阅读程序。当然,各有偏好和理由。

class ExplicitClass
    : public BaseClass
{
public:

初始化非常重要。不初始化基础或成员可能会产生警告,这是正确的,或者在某些情况下会捕获错误。因此,如果启用了该警告集合,这真的开始有意义,您将警告级别保持在较高水平,并且警告会倒计时。它还有助于表明意图:

    ExplicitClass()
        : BaseClass()   // <-- explicit call of base class constructor
    {
        // empty function
    }

在 IME 中,一个空的虚拟析构函数在统计上是导出 a 的最佳位置virtual(当然,如果对多个翻译可见,该定义将在其他地方)。您希望将其导出,因为有大量 rtti 和 vtable 信息可能最终导致二进制文件中不必要的膨胀。由于这个原因,我实际上非常定期地定义空析构函数:

    virtual ~ExplicitClass() {}  // <-- explicit empty virtual destructor

也许这是您团队中的约定,或者它记录了这正是实现的目的。这在大型代码库或复杂层次结构中也可能有帮助(主观),因为它还可以帮助提醒您该类型预期采用的动态接口。有些人更喜欢子类中的所有声明,因为他们可以在一个地方看到所有类的动态实现。因此,如果类层次结构/接口大于程序员的心理堆栈,则局部性可以帮助他们。像析构函数一样,这个 virtual 也可能是导出 typeinfo 的好地方:

    virtual void f() { BaseClass::f(); }  // <-- function just calls base
};

当然,如果您仅定义合格,则很难遵循程序或基本原理。因此,如果您只遵守约定,最终会得到一些更容易遵循的代码库,因为它比记录为什么每次都导出空析构函数更清楚。

最后一个原因(双向)是显式默认定义可以增加和减少构建和链接时间。

幸运的是,现在指定默认和删除的方法和构造函数更容易且明确。

于 2012-09-21T01:55:59.630 回答
0

由于问题是关于共识,我无法回答,但我发现 ildjarn 的评论很有趣且正确。

根据您的问题,是否有理由这样写,因为显式和隐式类的行为并不相同。人们有时出于“维护”的原因这样做,例如,如果派生f以不同的方式实现以记住调用基类。我个人觉得这没什么用。

于 2012-09-20T23:03:25.860 回答