为什么我允许声明一个删除了析构函数的对象?

Lig*_*ica 20 c++ language-lawyer c++11

请考虑以下文本:

[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
Run Code Online (Sandbox Code Playgroud)

GCC是否只是宽容?


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

#include <iostream>

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

struct B : A {};

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

int main() {}
Run Code Online (Sandbox Code Playgroud)

Seb*_*edl 13

第一部分不是格式错误,因为标准文本不适用 - 没有在那里声明类型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; }
}
Run Code Online (Sandbox Code Playgroud)

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

B::B() {
  A();
  try {
    // nothing to do here
  } catch(...) {
    ~A(); // error: ~A() is deleted.
    throw;
  }
}
Run Code Online (Sandbox Code Playgroud)

对于在完成某个子对象的初始化之后可能不会抛出异常的情况,使这项工作太复杂,无法指定.因此,实际上并不会发生这种情况,因为B的默认构造函数首先被隐式定义为已删除,这是由于N3797 12.1/4中的最后一个项目符号点:

如果出现以下情况,则将类X的默认默认构造函数定义为已删除:

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

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

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

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