为什么允许我声明一个带有已删除析构函数的对象

Why am I permitted to declare an object with a deleted destructor?

本文关键字:删除 析构函数 对象 一个 声明 允许我 为什么      更新时间:2023-10-16

请考虑以下文本:

[C++11: 12.4/11]: 析构函数被隐式调用

  • 对于程序终止时具有静态存储持续时间 (3.7.1) 的构造对象 (3.6.3),
  • 对于线程退出时线程存储持续时间为 (3.7.2) 的构造对象,
  • 对于具有自动存储持续时间 (3.7.3) 的构造对象,当创建对象的块退出 (6.7) 时,
  • 对于临时对象
  • 生存期结束时构造的临时对象 (12.2),
  • 对于由新表达式 (5.3.4) 分配的构造对象,通过使用删除表达式 (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

海湾合作委员会只是宽容吗?


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

#include <iostream>
struct A 
{
    A() {};
    ~A() = delete;
};
struct B : A {};
B *b = new B; // error: use of deleted function
int main() {}

第一部分不是格式不正确,因为标准文本不适用 - 那里没有声明类型 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 的默认构造函数定义为已删除:

  • [...]
  • 任何直接或虚拟基类或非静态数据成员都具有具有析构函数的类型,该析构函数已从默认的默认构造函数中删除或无法访问。

复制/移动构造函数的等效语言作为 12.8/11 中的第四个项目符号存在。

12.6.2/10 中还有一段重要段落:

在非委托构造函数中,可能会调用每个直接或虚拟基类以及类类型的每个非静态数据成员的析构函数。

B 的析构函数是由编译器在错误行生成的,并且它有一个对A 的析构函数的调用,该析构函数被删除,因此错误。 在第一个例子中,没有任何内容试图调用A的析构函数,因此没有错误。

我的猜测是,这就是发生的事情。

隐式生成的B()构造函数将首先构造其类型为 A 的基类子对象。然后,该语言声明,如果在执行 B() 构造函数的主体期间引发异常,则必须销毁A子对象。因此,需要访问已删除的~A() - 当构造函数抛出时正式需要它。当然,由于生成的B()体是空的,这永远不会发生,但~A()应该是可访问的要求仍然存在。

当然,这只是 1) 只是我这边对为什么首先存在错误的猜测,以及 2) 绝不是引用标准术语来说明这是否实际上是形式上的格式错误或只是 gcc 中的实现细节。也许可以给你一个线索,看看在标准中的位置......

可访问性与删除性正交:

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

有这个:

[C++11: 8.4.3/2]: 隐式或显式引用已删除函数(而不是声明它)的程序格式不正确。[ 注意:这包括隐式或显式调用函数,以及形成指向函数成员的指针或指针。它甚至适用于未潜在计算的表达式中的引用。如果函数重载,则仅当通过重载解析选择该函数时,才会引用该函数。—尾注 ]

但是您从不"引用"已删除的析构函数。

(我仍然无法解释为什么继承示例无法编译。