【问题标题】:Constructing (but not destucting) an object of a class with a deleted or non-user-provided private destructor构造(但不破坏)具有已删除或非用户提供的私有析构函数的类的对象
【发布时间】:2020-12-03 11:05:31
【问题描述】:

下面的sn-p代码格式正确吗?

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

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

乍一看,A 的已删除 dtor 和 B 的私有显式默认 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

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


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

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

class D { ~D() {} };
D *pd{new D{}};  // OK

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

struct E {
    ~E() = delete; 
private: 
    int x;
};
E *pe{new E{}};

其中E 不是聚合(私有数据成员)同样被拒绝,就像上面聚合类A 和B 一样,而下面F 和G 的示例(聚合直到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{}};

都被 GCC 接受。

【问题讨论】:

  • 你有什么问题?
  • @Eric ”Is there any open bug report [...]”(我显然没有找到),或者一个回答指出我错了,这个例子,出于某种原因, 实际上格式不正确。
  • @cigien 添加了 LL 标签。
  • @Phil1970 -- 你是对的,如果构造函数抛出异常,编译器必须展开已经构造的任何部分。这不涉及正在构造的对象的析构函数;该析构函数只能在成功构造后使用(想想你必须做些什么来编写一个可以处理部分构造的对象的析构函数)。编译器生成的代码会跟踪到目前为止已构建的内容,因此它只能销毁已成功构建的部分。

标签: c++ gcc language-lawyer compiler-bug


【解决方案1】:

sn-ps 格式正确;这是一个 GCC 错误 (59238)

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

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

[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])。

[...] 析构函数也可以通过使用 delete-expression ([expr.delete]) 用于由 new-expression ([expr.new]) 分配的构造对象; 上下文 调用是 delete-expression。

(15.1) 到 (15.4) 均不适用于此处,尤其是“对于由 new-expression 分配的构造对象” ,在这里确实适用,析构函数仅通过使用 delete-expression 隐式调用,我们在此示例中没有使用它。

[class.dtor]/15 的第二部分介绍了析构函数何时可能被调用,以及如果析构函数被潜在调用并被删除,则程序是非良构的:

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

在这种情况下没有显式调用析构函数。

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

[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 和 E 的示例是错误的。正如in a comment by @JeffGarrett 指出的那样,这看起来像以下打开的 GCC 错误报告:

我们可能会注意到,正如错误报告中指出的那样,GCC 在使用列表初始化分配时只会错误地拒绝这些程序,而以下修改的示例 A、B 和 E 都被海合会:

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()};

【讨论】:

    猜你喜欢
    • 2016-06-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-27
    • 2017-11-30
    • 1970-01-01
    相关资源
    最近更新 更多