POD и наследование в С++ 11. Имеет ли адрес struct == адрес первого элемента?
(Я отредактировал этот вопрос, чтобы избежать отвлечения внимания. Существует один ключевой вопрос, который нужно будет прояснить, прежде чем любой другой вопрос будет иметь смысл. Извиниться перед кем-либо, чей ответ теперь кажется менее актуальным.)
Задайте конкретный пример:
struct Base {
int i;
};
Нет виртуального метода, и наследования нет, и, как правило, это очень немой и простой объект. Следовательно, "Обычные старые данные" (POD), и он возвращается к прогнозируемому макету. В частности:
Base b;
&b == reinterpret_cast<B*>&(b.i);
Это соответствует Wikipedia (который, как утверждается, ссылается на стандарт С++ 03):
Указатель на объект POD-структуры, подходящим образом преобразованный с использованием реинтерпрета, указывает на его начальный элемент и наоборот, подразумевая, что в начале POD-структуры нет отступов. [8]
Теперь рассмотрим наследование:
struct Derived : public Base {
};
Опять же, нет виртуальных методов, нет виртуального наследования и нет множественного наследования. Поэтому это также POD.
Вопрос: Этот факт (Derived - POD в С++ 11) позволяет нам сказать, что:
Derived d;
&d == reinterpret_cast<D*>&(d.i); // true on g++-4.6
Если это так, то будет определено следующее:
Base *b = reinterpret_cast<Base*>(malloc(sizeof(Derived)));
free(b); // It will be freeing the same address, so this is OK
Я не спрашиваю о new
и delete
здесь - проще рассмотреть malloc
и free
. Мне просто интересно узнать о правилах создания объектов в простых случаях, подобных этому, и где исходный нестатический член базового класса находится в предсказуемом месте.
Является ли производным объект, который должен быть эквивалентен:
struct Derived { // no inheritance
Base b; // it just contains it instead
};
без прокладки заранее?
Ответы
Ответ 1
Вы не заботитесь о POD-ness, вы заботитесь о стандартном макете. Здесь определение из стандартного раздела 9 [class]
:
Класс стандартного макета - это класс, который:
- не имеет нестатических членов данных типа нестандартного макета класса (или массива таких типов) или ссылки,
- не имеет виртуальных функций (10.3) и нет виртуальных базовых классов (10.1),
- имеет тот же контроль доступа (раздел 11) для всех нестатических членов данных,
- не имеет базовых классов нестандартной компоновки,
- либо не имеет нестатических членов данных в самом производном классе и не более одного базового класса с нестатическими членами данных, либо не имеет базовых классов с нестатическими членами данных, а
- не имеет базовых классов того же типа, что и первый нестатический элемент данных.
И свойство, которое вы хотите, затем гарантируется (раздел 9.2 [class.mem]
):
Указатель на объект структуры стандартного макета, соответствующим образом преобразованный с помощью reinterpret_cast
, указывает на его начальный член (или если этот элемент является битовым полем, а затем блоку, в котором он находится) и наоборот.
Это действительно лучше старого требования, поскольку возможность reinterpret_cast
не теряется путем добавления нетривиальных конструкторов и/или деструктора.
Теперь перейдем к вашему второму вопросу. Ответ не в том, на что вы надеялись.
Base *b = new Derived;
delete b;
- это undefined поведение, если Base
не имеет виртуального деструктора. См. Раздел 5.3.5 ([expr.delete]
)
В первом альтернативе (удалить объект), если статический тип подлежащего удалению объекта отличается от его динамического типа, статический тип должен быть базовым классом динамического типа объекта, подлежащего удалению, и статическим тип должен иметь виртуальный деструктор или поведение undefined.
Ваш предыдущий фрагмент с использованием malloc
и free
в основном верен. Это будет работать:
Base *b = new (malloc(sizeof(Derived))) Derived;
free(b);
поскольку значение указателя b
совпадает с адресом, возвращаемым из нового места размещения, который, в свою очередь, является тем же адресом, который был возвращен из malloc
.
Ответ 2
Предположительно, ваш последний бит кода предназначен для того, чтобы сказать:
Base *b = new Derived;
delete b; // delete b, not d.
В этом случае короткий ответ заключается в том, что он остается undefined. Тот факт, что рассматриваемый класс или структура POD, стандартная компоновка или тривиально копируемая, ничего не меняет.
Да, вы передаете правильный адрес, и да, вы и я знаем, что в этом случае dtor в значительной степени ноль - тем не менее, указатель, который вы передаете на delete
, имеет другой статический тип чем динамический тип, а у статического типа нет виртуального dtor. Стандарт совершенно ясен, что дает поведение undefined.
С практической точки зрения, вы, возможно, избегаете UB, если вы действительно настаиваете - вероятность того, что не будет никаких вредных побочных эффектов от того, что вы делаете, по крайней мере, с большинством типичных компиляторов. Остерегайтесь, однако, что даже в лучшем случае код чрезвычайно хрупок, поэтому казалось бы, тривиальные изменения могут сломать все - и даже перейти на компилятор с действительно тяжелой проверкой типов, и это тоже может сделать.
Что касается вашего аргумента, ситуация довольно проста: это в основном означает, что комитет, возможно, мог бы сделать это определенное поведение, если захочет. Однако, насколько мне известно, он никогда не предлагался, и даже если бы это было, это, вероятно, было бы очень низкоприоритетным пунктом - на самом деле это не очень много, разрешить новые стили программирования и т.д.
Ответ 3
Это подразумевается как дополнение к ответу Бен-Voigt, а не замена.
Вы можете подумать, что это всего лишь техничность. То, что стандарт, называющий его "undefined", - это всего лишь немного семантической twaddle, которая не имеет реальных эффектов, не позволяя авторам компилятора делать глупые вещи без уважительной причины. Но это не так.
Я мог видеть желательные реализации, в которых:
Base *b = new Derived;
delete b;
В результате поведение было довольно странным. Это связано с тем, что сохранение размера выделенного фрагмента памяти, когда оно известно статически компилятором, выглядит глупым. Например:
struct Base {
};
struct Derived {
int an_int;
};
В этом случае, когда вызывается delete Base
, у компилятора есть все основания (из-за правила, которое вы цитировали в начале вашего вопроса), полагая, что размер указанных данных равен 1, а не 4. Если он, например, реализует версию operator new
, которая имеет отдельный массив, в котором все байтовые сущности все плотно упакованы, а другой массив, в котором 4 байтовых сущности все плотно упакованы, в конечном итоге предполагается, что Base *
указывает на место в массиве сущностей 1 байта, когда на самом деле он указывает где-то в 4-байтовый массив сущностей и делает по этой причине всевозможные интересные ошибки.
Я действительно хотел, чтобы operator delete
был определен так же, как и размер, и компилятор передал либо статически известный размер, если operator delete
был вызван на объект с не виртуальным деструктором, либо известный размер фактический объект, на который указывает, если он был вызван в результате деструктора virtual
. Хотя это, вероятно, будет иметь другие вредные последствия и, возможно, не такая хорошая идея (например, если есть случаи, в которых operator delete
вызывается без вызова деструктора). Но это сделало бы проблему болезненно очевидной.
Ответ 4
Существует много дискуссий по нерелевантным вопросам выше. Да, в основном для совместимости с C существует ряд гарантий, на которые вы можете положиться, пока вы знаете, что делаете. Все это, однако, не имеет отношения к вашему основному вопросу. Главный вопрос: существует ли какая-либо ситуация, когда объект может быть удален с использованием типа указателя, который не соответствует динамическому типу объекта и где у указанного типа нет виртуального деструктора. Ответ: нет, нет.
Логика для этого может быть получена из того, что должна выполнять система времени выполнения: она получает указатель на объект и его просят удалить. Ему нужно будет хранить информацию о том, как вызвать деструкторы производного класса или объем памяти, который фактически занимает объект, если это необходимо определить. Однако это подразумевает, возможно, довольно значительную стоимость с точки зрения используемой памяти. Например, если первый член требует очень строгого выравнивания, например. для выравнивания на границе 8 байтов, как это имеет место для double
, добавление размера добавило бы служебные данные не менее 8 байтов для распределения памяти. Хотя это может показаться не слишком плохим, это может означать, что только один объект вместо двух или четырех вписывается в строку кэша, что существенно снижает производительность.