为什么我可以声明一个具有已删除析构函数的对象?

20
考虑以下文本:

[C++11: 12.4/11]: 析构函数会在如下情况下被隐式调用:

  • 对于那些具有静态存储期(3.7.1)的已构造对象,它们在程序结束时(3.6.3)被销毁。
  • 对于那些具有线程存储期(3.7.2)的已构造对象,它们在所属线程结束时被销毁。
  • 对于那些具有自动存储期(3.7.3)的已构造对象,在创建这些对象的块退出时(6.7)被销毁。
  • 对于那些临时对象,在其生命周期结束时(12.2)被销毁。
  • 对于那些通过new-expression(5.3.4)分配的已构造对象,在使用delete-expression(5.3.5)删除时被销毁。
  • 在多种异常处理场景下也可能被销毁(15.3)。

如果声明了一个类对象或数组对象,而该类的析构函数不可访问,则程序是不合法的。析构函数还可以显式地调用。

那么为什么这个程序可以编译成功?

#include <iostream>

struct A 
{
    A(){ };
    ~A() = delete;
};

A* a = new A;

int main() {}

// g++ -std=c++11 -O2 -Wall -pedantic -pthread main.cpp && ./a.out

GCC只是宽容吗?


我倾向于这样说,因为它拒绝了以下内容,但标准似乎没有特定规则适用于继承层次结构中的已删除析构函数(唯一与之有关的措辞与默认的默认构造函数的生成有关):

#include <iostream>

struct A 
{
    A() {};
    ~A() = delete;
};

struct B : A {};

B *b = new B; // error: use of deleted function

int main() {}

没有时间进行标准潜水,但“可访问”并不等同于“未删除”。它是公开的。 - R. Martinho Fernandes
1
尽管如此,GCC仍然允许即使被设置为私有。 - R. Martinho Fernandes
2
@David:这两者严格等价。 - Lightness Races in Orbit
@ForEveR:因为(a)我引用了规定文本,指出了这一点(或在我意识到我错读之前是这样的),以及(b)它在继承层次结构中拒绝相同的操作,正如我在问题中所指出的。 - Lightness Races in Orbit
2
你的代码没有声明类型为 A 的对象或数组。 - Kerrek SB
显示剩余13条评论
4个回答

14

第一部分没有错误,因为标准文本不适用 - 在那里没有声明任何类型为A的对象。

关于第二部分,让我们回顾一下对象构造的工作原理。 标准规定(15.2/2),如果构造的任何部分抛出异常,那么在该点之前完全构造的所有子对象都将以相反的构造顺序被销毁。

这意味着,如果将构造函数的所有内容手动编写出来,代码看起来会像这样:

// Given:
struct C : A, B {
   D d;
   C() : A(), B(), d() { /* more code */ }
};

// This is the expanded constructor:
C() {
  A();
  try {
    B();
    try {
      d.D();
      try {
        /* more code */
      } catch(...) { d.~D(); throw; }
    } catch(...) { ~B(); throw; }
  } catch(...) { ~A(); throw; }
}

对于您更简单的类,用于默认构造函数(new表达式所需的定义)的扩展代码如下:

B::B() {
  A();
  try {
    // nothing to do here
  } catch(...) {
    ~A(); // error: ~A() is deleted.
    throw;
  }
}
由于无法在完成某个子对象的初始化后不可能抛出异常的情况下使其正常运行过于复杂,因此无法对其进行详细说明。因此,在N3797 12.1/4的最后一个要点中,B的默认构造函数被隐式定义为已删除,因此实际上不会发生这种情况:
“如果类X的任何直接或虚拟基类或非静态数据成员具有具有已删除或从默认构造函数不可访问的析构函数类型,则类X的默认默认构造函数被定义为已删除。”
与此类似的语言也适用于复制/移动构造函数,作为12.8/11中的第四个要点存在。
在12.6.2/10中还有一段重要的段落:
“在非委托构造函数中,每个直接或虚拟基类和每个类类型的非静态数据成员的析构函数都可能被调用。”

哦,异常是个好点。12.6.2/10似乎是我们在原问题中正在寻找的,但它只存在于C++14中,而不是C++11。 - Lightness Races in Orbit
考虑到12.6.2/10中提到的析构函数只有在构造函数不是“noexcept”时才会被潜在调用,这样做是否正确? - Anton Savin
@LightnessRacesinOrbit 这段话确实是C++14中的新内容,但由于它是通过一个缺陷报告(具体来说是DR1424)引入的,因此它仍适用于C++11,或者至少编译器开发人员普遍持有这种观点。http://open-std.org/JTC1/SC22/WG21/docs/cwg_defects.html#1424 - Sebastian Redl
很有可能。请参考DR1915,该内容正是要求这一点,但被视为语言扩展,因此讨论已移至EWG。http://open-std.org/JTC1/SC22/WG21/docs/cwg_closed.html#1915 - Sebastian Redl

3

问题在于编译器在错误行生成了B的析构函数,并调用被删除的A的析构函数,因此出现错误。在第一个示例中,没有任何尝试调用A的析构函数,因此没有错误。


为什么B()会“调用A的析构函数”?我在标准中找不到任何提示,而且这也违反直觉。 - Lightness Races in Orbit
1
我认为是这样的,因为编译器甚至无法为 B 生成默认析构函数,因为默认析构函数必须能够销毁 A(因此必须能够调用 A ::〜A())。 - vsoftco
2
编译器无法知道是否会调用 ~B()(停机问题),因此它会在您的错误行生成一个(除非您使用“delete”告诉它不要这样做)。 - Paul Evans
不是这样的。没有人要求编译器确定在任何动态执行中是否调用了析构函数(这归结为停机问题)。唯一的问题是析构函数是否被提及,对于编译器来说这是微不足道的。第二个示例中的代码从未引用B的析构函数。 - Sebastian Redl
1
@SebastianRedl 但是第二个例子中的代码包含B的析构函数。只有链接器才知道是否实际调用了析构函数,因此编译器无法选择不创建它(除非使用=delete进行显式声明)。 - hyde
显示剩余3条评论

3
我猜这是发生的事情。
隐式生成的B()构造函数首先会构造其类型为A的基类子对象。然后,语言规定如果在执行B()构造函数体期间抛出异常,则必须销毁A子对象。因此需要访问已删除的~A() - 在构造函数抛出异常时正式需要它。当然,由于B()的生成体为空,这永远不会发生,但仍然需要访问~A()
当然,这只是我的猜测,为什么一开始就会出现错误,并且不以任何方式引用标准,以确定是否实际上是形式上非法或者只是gcc中的实现细节。也许可以给你一个线索,在标准中查找的位置...

1

无论是否删除,可访问性都是正交的:

[C++11: 11.2/1]: 如果一个类使用 public 访问修饰符声明为另一个类的基类(第10条),则该基类的 public 成员可以作为派生类的 public 成员进行访问,而基类的 protected 成员可以作为派生类的 protected 成员进行访问。如果一个类使用 protected 访问修饰符声明为另一个类的基类,则该基类的 publicprotected 成员可以作为派生类的 protected 成员进行访问。如果一个类使用 private 访问修饰符声明为另一个类的基类,则该基类的 publicprotected 成员可以作为派生类的 private 成员进行访问。

还有这个:

: 如果程序隐式或显式地引用已删除的函数(除了声明它),则该程序是非法的。[注:这包括隐式或显式调用该函数以及形成指向该函数的指针或指向成员的指针。即使在不会被潜在评估的表达式中引用也适用。如果函数是重载的,则仅在通过重载解析选择该函数时才引用。——结束说明] 但您从未“引用”已删除的析构函数。(我仍然无法解释为什么继承示例无法编译。)

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