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

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

[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() {}

4 ответа

Решение

Первая часть не является неправильной, потому что стандартный текст не применяется - там не объявляется объект типа А.

Для второй части давайте рассмотрим, как работает строительство объекта. Стандарт гласит (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 определяется как удаленный, если:

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

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

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

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

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

Я думаю, что это то, что происходит.

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

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

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

[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]: Программа, которая ссылается на удаленную функцию неявно или явно, кроме как для ее объявления, является неправильно сформированной. [Примечание: это включает в себя вызов функции неявно или явно и формирование указателя или указателя на член функции. Это относится даже к ссылкам в выражениях, которые потенциально не оцениваются. Если функция перегружена, на нее ссылаются, только если функция выбрана разрешением перегрузки. —Конечная записка]

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

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

Другие вопросы по тегам