编译器在析构函数省略方面的自由度是多少?

15

众所周知,在某些条件下编译器可能会省略对复制构造函数的调用。然而,标准明确表示编译器只能自由更改运行时行为(调用或不调用复制构造函数),但翻译过程中仍需假定调用了复制构造函数。特别地,编译器会检查是否存在可以调用的有效复制构造函数。

我遇到一种情况,析构函数调用可能被省略,但编译器在是否需要存在有效析构函数方面存在不同。

以下是一个完整的示例,展示了此问题可能发生的情况以及编译器行为的差异。

template <typename T>
struct A {
  ~A() { (void) sizeof(T); }
};

struct B;    // defined elsewhere.

struct C {
  A<B> x, y;
  ~C();      // defined in a TU where B is complete.
};

int main() {
  C c;
}

在编译main()时,编译器会生成C的默认构造函数。该构造函数首先对x进行默认初始化,然后再对y进行初始化。如果在y构造过程中抛出异常,则必须销毁x。生成的代码如下:

new ((void*) &this->x) A<B>;   // default initializes this->x.
try {
  new ((void*) &this->y) A<B>; // default initializes this->y.
}
catch (...) {
  (this->x).~A<B>();           // destroys this->x.
  throw;
}

了解到A<B>的默认构造函数是平凡的(不会抛出异常),根据as-if规则,编译器可能简化代码为:

new ((void*) &this->x) A<B>;   // default initializes this->x.
new ((void*) &this->y) A<B>;   // default initializes this->y.

因此,没有必要调用~A<B>()。(实际上,编译器甚至可以删除上面的两个初始化,因为A<B>的构造函数是平凡的,但这对本讨论不重要。)
问题是:即使可以省略调用析构函数,编译器是否应该验证是否有有效的析构函数?我在标准中找不到任何澄清此事的内容。能否提供相关引用?
如果编译器决定不翻译~A<B>()(像gcc和Visual Studio一样),则编译成功。
然而,如果编译器仍然决定翻译~A<B>()(像clang和icc一样),那么它会引发错误,因为这里的B是不完整的类型,无法获取其大小。

析构函数的调用从未被省略,至少不像复制构造函数的调用可以被省略那样。复制构造函数的省略将允许编译器丢弃副作用。这是编译器允许做到这一点的唯一情况。至于会发生什么,我不是完全确定,但我想它们不会在此编译单元中实例化析构函数,因为它从未直接从中调用。如果您在main中创建一个A<B>,gcc将拒绝编译。 - zneak
就像@zneak所说的那样。当您编译包含“〜C”定义的TU时,您将获得错误诊断。 - Casey
2
@Casey 已经说明了析构函数在 B 完成的 TU 中定义 - 在这种情况下,获取其大小不会出错。 - Alan Stokes
1个回答

1

我认为标准中没有指定这一点。如果实例化了~A<B>,则是非法的,并且需要进行诊断。正如您所说,如果构造y引发异常,则必须销毁x

然而,构造y永远不会引发异常,因此可以说永远不需要存在析构函数的定义(参见15.2/2、14.7.1/3)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接