Почему мне разрешено объявлять объект с удаленным деструктором?
Рассмотрим следующий текст:
[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]
Но вы никогда не ссылаетесь на удаленный деструктор.
(Я все еще не могу объяснить, почему пример наследования не компилируется.)