Почему мне разрешено объявлять объект с удаленным деструктором?

Рассмотрим следующий текст:

[C++11: 12.4/11]: Деструкторы вызываются неявно

  • для построенных объектов со статической продолжительностью хранения (3.7.1) при завершении программы (3.6.3),
  • для построенных объектов с длительностью хранения потоков (3.7.2) при выходе потока,
  • для построенных объектов с автоматическим временем хранения (3.7.3), когда блок, в котором создается объект, завершает (6.7),
  • для построенных временных объектов, когда срок жизни временного объекта заканчивается (12.2),
  • для построенных объектов, выделенных новым выражением (5.3.4), с использованием выражения-удаления (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

Является ли GCC просто разрешительным?


Я склонен это сказать, так как он отклоняет следующий стандарт, по-видимому, не имеет конкретного правила, специфичного для удаленных деструкторов в иерархии наследования (единственное достаточно важная формулировка относится к генерации дефолтных конструкторов по умолчанию):

#include <iostream>

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

struct B : A {};

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

int main() {}

Ответ 1

Первая часть не плохо сформирована, потому что стандартный текст не применяется - объект типа 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; }
}

Для вашего более простого класса расширенный код для конструктора по умолчанию (определение которого требуется выражением new) будет выглядеть так:

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

Выполнение этой работы для случаев, когда невозможно исключить исключение после инициализации для некоторого подобъекта, слишком сложно определить. Поэтому это фактически не происходит, потому что конструктор по умолчанию для B неявно определяется как удаленный в первую очередь из-за последней точки маркера в N3797 12.1/4:

По умолчанию конструктор по умолчанию для класса X определяется как удаленный, если:

  • [...]
  • любой прямой или виртуальный базовый класс или нестатический член данных имеет тип с деструктором, который удален или недоступен из стандартного конструктора по умолчанию.

Эквивалентный язык существует для конструкторов copy/move в качестве четвертой пули в 12.8/11.

В 12.6.2/10 есть важный пункт:

В конструкторе без делегирования потенциально вызывается деструктор для каждого прямого или виртуального базового класса и для каждого нестатического члена данных типа класса.

Ответ 2

Это то, что деструктор B генерируется компилятором в строке вашей ошибки и имеет вызов A destructor, который удаляется, следовательно, ошибка. В первом примере ничто не пытается вызвать A destructor, следовательно, нет ошибки.

Ответ 3

Мое предположение - это то, что происходит.

Неявно сгенерированный конструктор B() прежде всего построит подобъект своего базового класса типа A. Затем в этом языке указано, что если при выполнении тела конструктора B() возникает исключение, подобъект A должен быть уничтожен. Следовательно, необходимо получить доступ к удалённому ~A() - это формально необходимо, когда бросается конструктор. Конечно, поскольку сгенерированное тело B() пустое, это никогда не может произойти, но требование, чтобы ~A() должно быть доступно, все еще существует.

Конечно, это 1) просто догадка с моей стороны, почему в первую очередь возникает ошибка, и 2) никоим образом не цитирование стандартного вопроса о том, действительно ли это будет формально плохо сформировано или просто деталь реализации в gcc. Возможно, вы дадите вам ключ от того, где в стандарте выглядеть, хотя...

Ответ 4

Доступность ортогональна к удалению:

[C++11: 11.2/1]: Если класс объявлен базовым классом (раздел 10) для другого класса с использованием спецификатора доступа public, члены public базового класса доступны как public члены производного класс и protected члены базового класса доступны как члены protected производного класса. Если класс объявлен базовым классом для другого класса с использованием спецификатора доступа protected, члены базового класса public и protected доступны как члены protected производного класса. Если класс объявлен базовым классом для другого класса с использованием спецификатора доступа private, члены базового класса public и protected доступны как private членов производного класса.

Существует следующее:

[C++11: 8.4.3/2]: Программа, которая ссылается на удаленную функцию неявно или явно, кроме объявления ее, плохо сформирована. [Примечание. Это включает вызов функции неявно или явно и формирование указателя или указателя на элемент функции. Он применяется даже для ссылок в выражениях, которые потенциально не оцениваются. Если функция перегружена, она ссылается только в том случае, если функция выбрана с помощью разрешения перегрузки. -end note]

Но вы никогда не ссылаетесь на удаленный деструктор.

(Я все еще не могу объяснить, почему пример наследования не компилируется.)