Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

Consider the following text:

[C++11: 12.4/11]: Destructors are invoked implicitly

  • for constructed objects with static storage duration (3.7.1) at program termination (3.6.3),
  • for constructed objects with thread storage duration (3.7.2) at thread exit,
  • for constructed objects with automatic storage duration (3.7.3) when the block in which an object is created exits (6.7),
  • for constructed temporary objects when the lifetime of a temporary object ends (12.2),
  • for constructed objects allocated by a new-expression (5.3.4), through use of a delete-expression (5.3.5),
  • in several situations due to the handling of exceptions (15.3).

A program is ill-formed if an object of class type or array thereof is declared and the destructor for the class is not accessible at the point of the declaration. Destructors can also be invoked explicitly.

Then why does this program compile successfully?

#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

Is GCC just being permissive?


I'm inclined to say so, since it rejects the following yet the standard appears to have no particular rule specific to deleted destructors in an inheritance hierarchy (the only loosely relevant wording is pertinent to the generation of defaulted default constructors):

#include <iostream>

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

struct B : A {};

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

int main() {}
like image 473
Lightness Races in Orbit Avatar asked Nov 18 '14 12:11

Lightness Races in Orbit


3 Answers

The first part is not ill-formed because the standard text doesn't apply - no object of type A is declared there.

For the second part, let's review how object construction works. The standard says (15.2/2) that if any part of construction throws, all fully constructed subobjects up to that point are destroyed in reverse order of construction.

This means that the code underlying a constructor, if all written out by hand, would look something like this:

// 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; }
}

For your simpler class, the expanded code for the default constructor (whose definition is required by the new expression) would look like this:

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

Making this work for cases where no exception can possibly be thrown after initialization for some subobject has been completed is just too complex to specify. Therefore, this doesn't actually happen, because the default constructor for B is implicitly defined as deleted in the first place, due to the last bullet point in N3797 12.1/4:

A defaulted default constructor for class X is defined as deleted if:

  • [...]
  • any direct or virtual base class or non-static data member has a type with a destructor that is deleted or inaccessible from the defaulted default constructor.

The equivalent language exists for copy/move constructors as the fourth bullet in 12.8/11.

There is also an important paragraph in 12.6.2/10:

In a non-delegating constructor, the destructor for each direct or virtual base class and for each non-static data member of class type is potentially invoked.

like image 114
Sebastian Redl Avatar answered Oct 21 '22 11:10

Sebastian Redl


It's that B's destructor is generated by the compiler at the line of your error and it has a call to A's destructor which is deleted, hence the error. In the first example nothing is trying to call A's destructor hence no error.

like image 26
Paul Evans Avatar answered Oct 21 '22 12:10

Paul Evans


My guess is that this is what happens.

The implicitly generated B() constructor will first of all construct its base class subobject of type A. The language then states that if an exception is thrown during execution of the body of the B() constructor, the A subobject must be destroyed. Hence the need to access the deleted ~A() - it is formally needed for when the constructor throws. Of course, since the generated body of B() is empty this can never ever happen, but the requirement that ~A() should be accessible is still there.

Of course, this is 1) just a guess from my side of why there is an error in the first place and 2) not in any way a quote of the standardese to say whether this would actually be formally ill-formed or just an implementation detail in gcc. Could perhaps give you a clue of where in the standard to look though...

like image 32
CAdaker Avatar answered Oct 21 '22 10:10

CAdaker