普通 ctor(或 dtor)和用户定义的空 ctor(或 dtor)有啥区别

Posted

技术标签:

【中文标题】普通 ctor(或 dtor)和用户定义的空 ctor(或 dtor)有啥区别【英文标题】:What is the difference between a trivial ctor (or dtor) and a user defined empty ctor (or dtor)普通 ctor(或 dtor)和用户定义的空 ctor(或 dtor)有什么区别 【发布时间】:2016-10-10 04:09:57 【问题描述】:

当类具有普通构造函数和/或普通析构函数时,C++ 标准定义了一些非常具体的行为。

例如,根据标准的 §3.8/1:

T 类型对象的生命周期结束于:

—如果T 是具有非平凡析构函数的类类型(12.4),则析构函数调用开始,或者

——对象占用的存储空间被重用或释放。

所以,

如果一个对象不是一般可破坏的,则在调用析构函数后访问该对象成员的任何尝试都是 UB。 如果一个对象很容易被破坏,那么在调用析构函数后尝试访问该对象的成员是安全的,而不是 UB。

虽然这个例子可能不是最好的例子,但它表明行为的差异可能是至关重要的(UB/non-UB),无论一个对象是否可以简单地破坏。

标准的第 12.4/3 条规定(概括而言)类 T 的析构函数是微不足道的,如果它是隐式定义,而不是虚拟的,并且如果它的所有基类和成员T 类可以轻易破坏。

根据我的(适度的)经验,就编译器生成的代码而言,我从未发现:

具有普通默认 ctor 和/或普通 dtor 的类,以及 具有用户定义的空 ctor和/或非虚拟用户定义的空 dtor的类(只要该类、其基类和成员类也有非虚拟 dtor 用户定义为空或琐碎)

所以,我的问题是:

在编译器代码生成、优化、权衡等方面,用户定义的空 ctor/dtor 可以或不可以被视为类似琐碎的 ctor/dtor... 与用户定义的非空ctor/dtor 相同的问题;在 ctor/dtor 中实现的代码应该遵循哪些规则才能将它们视为微不足道的。

我的问题与标准无关(请不要回答标准说明什么是微不足道的 ctor/dtor,因此用户定义的 ctor/dtor 不是),而是与编译器处理用户定义的 ctor/dtor 的方式以及与普通的 ctor/dtor 相比,编译代码的行为可能会以何种方式改变(或不改变)。

【问题讨论】:

【参考方案1】:

您比我更了解标准,但是根据您提供的信息,标准定义了一个微不足道的析构函数,但它没有定义一个空的析构函数,这会使这个问题产生误导。微不足道的析构函数是编译器可以优化的特殊情况,虽然空构造函数对我们有意义,但编译器编写者不必考虑。

浏览几个 SO 链接:

Why Does Default user defined destructors in C++ increases execution time? 显示了编译器对普通析构函数和空析构函数的行为不同的情况。那里的答案暗示一个区别是异常处理。它不会寻找空的构造函数,因为它不是必需的,因此会处理异常,就好像 dtor 中有有效代码一样。 Will an 'empty' constructor or destructor do the same thing as the generated one? 似乎与您的问题非常匹配,以至于它可能是重复的。最好自己阅读而不是依赖我的解释,但它提到了 Microsoft 编译器无法内联空析构函数,并且所有编译器都想要一个工作的基类析构函数(这对于base dtor 不是虚拟的)。

要回答您的第二个问题,只要您的 ctor 不为空,这并非易事。最接近琐碎的是一个空的 ctor/dtor,您仔细阅读标准已经告诉您,这并没有被定义为琐碎。

TL;DR: 该标准定义了一个普通的 dtor,但不是一个空的。智能编译器可以选择注意到它是用户定义的空并将其视为微不足道的,但标准不需要任何此类考虑。

【讨论】:

【参考方案2】:

在编译器代码生成、优化、权衡等方面,用户定义的空 ctor/dtor 可以或不可以被视为普通的 ctor/dtor...

如果构造函数/析构函数没有被内联,那么编译器可能(取决于链接时优化)必须向它们发出调用,即使它们是无操作的。

例如下面的代码:

struct Struct 
  Struct();
  ~Struct();
;

int main() 
  Struct s;

编译为(开启优化):

main:
        subq    $24, %rsp
        leaq    15(%rsp), %rdi
        call    Struct::Struct()
        leaq    15(%rsp), %rdi
        call    Struct::~Struct()
        xorl    %eax, %eax
        addq    $24, %rsp
        ret

请注意,仍然有对构造函数和析构函数的调用,即使在单独的文件中我可以将它们定义为空函数。

但是,如果您已内联定义:

struct Struct 
  Struct() 
  ~Struct() 
;

Struct foo() 
  return Struct;

然后编译器可以(如果它不完全糟糕的话,也会)将它们视为微不足道的构造函数/析构函数:

foo():
        movq    %rdi, %rax
        ret

在该示例中,所有构造函数/析构函数调用都被完全优化掉了,生成的代码与Struct 的定义是简单的struct Struct ; 相同。

与用户定义的非空 ctor/dtor 相同的问题;在 ctor/dtor 中实现的代码应该遵循哪些规则才能将它们视为微不足道的。

这种取决于。同样,如果构造函数/析构函数没有被内联,那么编译器可能仍然需要对它们发出调用,在这种情况下它们根本就不是微不足道的。

但是,如果优化器可以完全优化掉内联非空构造函数/析构函数(例如,如果它们只包含for (int x = 0; x < 1000; ++x);,那么这是可以优化掉的无用代码) ) 以至于它们实际上是空的。

但是,如果他们做了一些不能被优化掉的有用工作,那么他们就不会是微不足道的。他们将被运行。他们必须这样做。

【讨论】:

以上是关于普通 ctor(或 dtor)和用户定义的空 ctor(或 dtor)有啥区别的主要内容,如果未能解决你的问题,请参考以下文章

用于 arb 的静态 ctor/dtor 观察者。 C++ 类

转载C++基本功和 Design Pattern系列 ctor & dtor

ctor/dtor 与线程安全

Ptex源码学习笔记-1

条款19:设计class犹如设计type

用户定义的 Copy ctor 和 copy-ctors 进一步向下链编译器错误?程序员脑筋急转弯?