使用已删除或非用户提供的私有析构函数构造(但不破坏)类的对象

dfr*_*fri 6 c++ gcc language-lawyer compiler-bug

以下代码片段格式正确吗?

struct A { ~A() = delete; };
A *pa{new A{}};

class B { ~B() = default; };
B *pb{new B{}};
Run Code Online (Sandbox Code Playgroud)

A乍一看,B似乎从未使用过的已删除 dtor和私有显式默认 dtor从未使用过(有意的内存泄漏,如果您愿意的话),这可以说是格式良好的。

Clang 接受各种编译器和 C++ 版本(C++11 到 C++2a)的程序。

另一方面,GCC 拒绝针对各种编译器和 C++ 版本的程序。

struct A { ~A() = delete; };
A *pa{new A{}};  // GCC error: use of deleted function 'A::~A()'

class B { ~B() = default; };
B *pb{new B{}};  // GCC error: 'B::~B()' is private within this context
Run Code Online (Sandbox Code Playgroud)

(如果格式正确;在我提交错误报告之前:是否有针对此极端情况的任何开放的 GCC 错误报告?我自己搜索了 GCC:s bugzilla 无济于事。)


特别是 GCC 接受私有用户提供的析构函数的情况:

class C { ~C(); };
C *pc{new C{}};  // OK

class D { ~D() {} };
D *pd{new D{}};  // OK
Run Code Online (Sandbox Code Playgroud)

这可能暗示某些 GCC 为聚合类做一些特殊的事情,因为AB是聚合而CD不是。但是 GCC 与此行为不一致,如下例所示

struct E {
    ~E() = delete; 
private: 
    int x;
};
E *pe{new E{}};
Run Code Online (Sandbox Code Playgroud)

其中E一个非集合(私人数据成员)同样拒绝像为聚合类AB以上,而的实施例FG下面(的集合体,直到C ++ 20,和非聚集体,分别地)

struct F {
    F() = default;
    ~F() = delete; 
};
F *pf{new F{}};

struct G {
    G() = default;
    ~G() = delete; 
private: 
    int x;
};
G *pg{new G{}};
Run Code Online (Sandbox Code Playgroud)

都被 GCC 接受。

dfr*_*fri 1

片段结构良好;这是一个 GCC 错误 ( 59238 )

首先,[class.dtor]/4明确提到可以删除给定类的选定析构函数:

在类定义结束时,在该类中声明的预期析构函数之间执行重载决策,并使用空参数列表来选择该类的析构函数,也称为选定的析构函数。[...]析构函数选择不构成对所选析构函数的引用或odr 使用([basic.def.odr]) ,特别是,可以删除所选析构函数([dcl.fct.def 。删除])。

[class.dtor]/15控制在哪些情况下隐式调用析构函数;从第一部分开始:

隐式调用析构函数

  • (15.1)对于在程序终止([basic.start.term])时具有静态存储持续时间([basic.stc.static])的构造对象,
  • (15.2)对于线程退出时具有线程存储持续时间 ([basic.stc.thread]) 的构造对象,
  • (15.3)对于具有自动存储持续时间的构造对象([basic.stc.auto]),当创建对象的块存在时([stmt.dcl]),
  • (15.4)对于构造的临时对象,当其生命周期结束时([conv.rval],[class.temporary])。

[...] 析构函数也可以通过使用 new表达式([expr.new])分配的构造对象的删除表达式([expr.delete]) 隐式调用;调用的上下文是删除表达式

(15.1)(15.4)都不适用于此,特别是“对于由new 表达式分配的构造对象”,这确实适用于此,仅通过使用delete 表达式隐式调用析构函数,我们是本例中未使用。

[class.dtor]/15的第二部分涵盖了析构函数何时可能被调用,以及如果析构函数可能被调用并被删除,则程序是格式错误的:

析构函数也可以被显式调用。如果析构函数被调用或在 [expr.new]、[stmt.return]、[dcl.init.aggr]、[class.base.init] 和 [ except.throw] 中指定,则可能会调用该析构函数。 如果可能调用的析构函数被删除或无法从调用上下文访问,则程序是格式错误的。

在这种情况下,不会显式调用析构函数。

[expr.new](特别是指[expr.new]/24)此处不适用,因为它仅与创建类类型的对象数组时相关(使用new-expression)。

[stmt.return]在这里不适用,因为它涉及 return 语句中构造函数和析构函数的(可能)调用。

[dcl.init.aggr](特别是[dcl.init.aggr]/8)此处不适用,因为它涉及聚合元素的潜在调用的析构函数而不是聚合类本身的潜在调用的析构函数。

[class.base.init]在这里不适用,因为它涉及(基类)子对象的潜在调用的析构函数。

[ except.throw](特别是[ except. throw] / 3[ except. throw] / 5)在这里不适用,因为它涉及异常对象的潜在调用的析构函数。

因此,[class.dtor]/15 都不适用于这种情况,并且 GCC 拒绝OP 中A,B和的示例是错误的。正如@JeffGarrett 在评论中E指出的,这看起来像以下开放的 GCC 错误报告:

我们可能会注意到,正如错误报告中指出的那样,GCC 仅在使用列表初始化分配时错误地拒绝了这些程序,而以下修改后的示例ABE均被 GCC 接受:

struct A { ~A() = delete; };
A *pa{new A()};

class B { ~B() = default; };
B *pb{new B()};

struct E {
    ~E() = delete; 
private: 
    int x;
};
E *pe{new E()};
Run Code Online (Sandbox Code Playgroud)