Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Constructing (but not destucting) an object of a class with a deleted or non-user-provided private destructor

Is the following snippet of code well-formed?

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

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

At a first glace it would seems as if the deleted dtor of A and the private explicitly-defaulted dtor of B are never used (intentional memory leak, if you will), which would arguably imply that it is well-formed.

Clang accepts the program for various compiler and C++ versions (C++11 through C++2a).

GCC, on the other hand, rejects the program for various compiler and C++ versions.

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

(It if well-formed; before I file a bug report: Is there any open GCC bug report for this corner case? I've searched GCC:s bugzilla myself to no avail.)


Peculiarly GCC accepts the case of a private user-provided destructor:

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

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

which could hint for some GCC doing something special for aggregate classes, as A and B are aggregates whereas C and D are not. However GCC is no consistent with this behaviour, as the following example

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

where E is not an aggregate (private data member) is likewise rejected like the for the aggregate classes A and B above, whereas the examples of F and G below (an aggregate until C++20, and a non-aggregate, respectively)

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

struct G {
    G() = default;
    ~G() = delete; 
private: 
    int x;
};
G *pg{new G{}};

are both accepted by GCC.

like image 756
dfrib Avatar asked Dec 03 '20 11:12

dfrib


1 Answers

The snippets are well-formed; this is a GCC bug (59238)

To begin with, [class.dtor]/4 explicitly mentions that the selected destructor for a given class may be deleted:

At the end of the definition of a class, overload resolution is performed among the prospective destructors declared in that class with an empty argument list to select the destructor for the class, also known as the selected destructor. [...] Destructor selection does not constitute a reference to, or odr-use ([basic.def.odr]) of, the selected destructor, and in particular, the selected destructor may be deleted ([dcl.fct.def.delete]).

[class.dtor]/15 governs for which cases a destructor is invoked implicitly; starting with the first part:

A destructor is invoked implicitly

  • (15.1) for a constructed object with static storage duration ([basic.stc.static]) at program termination ([basic.start.term]),
  • (15.2) for a constructed object with thread storage duration ([basic.stc.thread]) at thread exit,
  • (15.3) for a constructed object with automatic storage duration ([basic.stc.auto]) when the block in which an object is created exits ([stmt.dcl]),
  • (15.4) for a constructed temporary object when its lifetime ends ([conv.rval], [class.temporary]).

[...] A destructor may also be invoked implicitly through use of a delete-expression ([expr.delete]) for a constructed object allocated by a new-expression ([expr.new]); the context of the invocation is the delete-expression.

None of (15.1) through (15.4) applies here, and particularly "for a constructed object allocated by a new-expression", which does apply here, a destructor is invoked implicitly only through the use of a delete-expression, which we are not using in this example.

The second part of [class.dtor]/15 covers when a destructor is potentially invoked, and that a program is ill-formed if a destructor is potentially invoked and is deleted:

A destructor can also be invoked explicitly. A destructor is potentially invoked if it is invoked or as specified in [expr.new], [stmt.return], [dcl.init.aggr], [class.base.init], and [except.throw]. A program is ill-formed if a destructor that is potentially invoked is deleted or not accessible from the context of the invocation.

No destructor is invoked explicitly in this case.

[expr.new] (particularly referring to [expr.new]/24) does not apply here, as it relates only to when creating an array of objects of class type (using a new-expression).

[stmt.return] does not apply here, as it relates to (possible) invocation of constructors and destructors in return statements.

[dcl.init.aggr] (particularly [dcl.init.aggr]/8) does not apply here as it relates to the potentially invoked destructors of the elements of the aggregate, and not a potentially invoked destructor of the aggregate class itself.

[class.base.init] does not apply here as it relates to potentially invoked destructors of (base class) subobjects.

[except.throw] (particularly [except.throw]/3 and [except.throw]/5) does not apply here as it relates to the potentially invoked destructor of an exception object.

Thus, none of [class.dtor]/15 applies in this case, and GCC is wrong to reject the examples of A, B and E in the OP. As pointed out in a comment by @JeffGarrett, this looks like the following open GCC bug report:

  • Bug 59238 - Dynamic allocating a list-initialized object of a type with private destructor fails.

And we may note, as pointed out in the bug report, that GCC only wrongly rejects these program when allocating using list-initialization, whereas the following modified examples A, B and E are all accepted by 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()};
like image 68
dfrib Avatar answered Sep 30 '22 08:09

dfrib