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 байтов для распределения памяти. Хотя это может показаться не слишком плохим, это может означать, что только один объект вместо двух или четырех вписывается в строку кэша, что существенно снижает производительность.