POD и наследование в C ++ 11.Имеет ли адрес структура == адрес первого члена? - PullRequest
7 голосов
/ 14 января 2012

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

Давайте настроим конкретный пример:

struct Base {
    int i;
};

Нет виртуального метода, нет наследования, и, как правило, это очень тупой и простой объект. Следовательно, это Обычные старые данные (POD) , и оно возвращается к предсказуемому расположению. В частности:

Base b;
&b == reinterpret_cast<B*>&(b.i);

Это согласно Википедии (которая сама утверждает, что ссылается на стандарт C ++ 03):

Указатель на объект POD-структуры, соответствующим образом преобразованный с использованием переинтерпретации, указывает на его начальный член и наоборот, подразумевая, что в начале POD-структуры нет заполнения. [8]

Теперь давайте рассмотрим наследование:

struct Derived : public Base {
};

Опять же, здесь нет виртуальных методов, виртуального наследования и множественного наследования. Поэтому это также POD.

Вопрос: Позволяет ли этот факт (Derived является POD в C ++ 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
};

без заполнения заранее?

Ответы [ 4 ]

14 голосов
/ 14 января 2012

Вы не заботитесь о POD-сущности, вы заботитесь о стандартном макете .Вот определение из стандартного раздела 9 [class]:

Класс стандартного макета - это класс, который:

  • не имеет нестатических элементов данных типакласс нестандартной компоновки (или массив таких типов) или ссылка,
  • не имеет виртуальных функций (10.3) и виртуальных базовых классов (10.1),
  • имеет одинаковое управление доступом (Пункт 11) для всех нестатических элементов данных
  • не имеет базовых классов нестандартной компоновки,
  • либо не имеет нестатических элементов данных в наиболее производном классе и не более одногобазовый класс с нестатическими элементами данных или не имеет базовых классов с нестатическими элементами данных, а
  • не имеет базовых классов того же типа, что и первый нестатический элемент данных.

И тогда требуемое свойство гарантируется (раздел 9.2 [class.mem]):

Указатель на объект структуры стандартной компоновки, соответствующим образом преобразованный с использованием reinterpret_cast,указывает на его начальный элемент (или, если этот элемент является битовым полем, тоединица, в которой он находится) и наоборот.

Это на самом деле лучше, чем старое требование, потому что способность reinterpret_cast не теряется при добавлении нетривиальных конструкторов и / или деструктора.


Теперь перейдем ко второму вопросу.Ответ не тот, на который вы надеялись.

Base *b = new Derived;
delete b;

- неопределенное поведение, если только у Base нет виртуального деструктора.См. Раздел 5.3.5 ([expr.delete])

В первом варианте (удаление объекта), если статический тип удаляемого объекта отличается от его динамического типа, статический тип должен бытьбазовый класс динамического типа объекта, который нужно удалить, и статический тип должен иметь виртуальный деструктор или поведение не определено.


Ваш предыдущий фрагмент с использованием malloc и free в основном правильно.Это будет работать:

Base *b = new (malloc(sizeof(Derived))) Derived;
free(b);

, поскольку значение указателя b совпадает с адресом, возвращаемым из нового места размещения, что, в свою очередь, совпадает с адресом, возвращенным из malloc.

2 голосов
/ 15 января 2012

Это подразумевается как дополнение к ответу Бен Фойгта ', а не замене.

Вы можете подумать, что это всего лишь техническая часть.То, что стандарт, называющий его «неопределенный», - это всего лишь небольшая семантическая парадигма, которая не имеет реальных эффектов, за исключением того, что авторы компиляторов могут делать глупые вещи без веской причины.Но это не тот случай.

Я мог видеть желательные реализации, в которых:

Base *b = new Derived;
delete b;

Результатом было довольно странное поведение.Это связано с тем, что хранить размер выделенного фрагмента памяти, когда он статически известен компилятору, довольно глупо.Например:

struct Base {
};

struct Derived {
   int an_int;
};

В этом случае, когда вызывается delete Base, у компилятора есть все основания (из-за правила, которое вы указали в начале вашего вопроса), полагать, что размер данныхуказывает на 1, а не 4. Если он, например, реализует версию operator new, которая имеет отдельный массив, в котором 1-байтовые объекты все плотно упакованы, и другой массив, в котором 4-байтовые объекты все плотно упакованы,в конечном итоге он будет предполагать, что Base * указывает куда-то в 1-байтовом массиве сущностей, хотя на самом деле он указывает где-то в 4-байтовом массиве сущностей, и по этой причине совершает всевозможные интересные ошибки.

Я действительно хотел бы, чтобы operator delete был определен так, чтобы он также принимал размер, и компилятор передавал либо статически известный размер, если operator delete был вызван для объекта с не-виртуальным деструктором, либо известный размер фактического существа, являющегося объектом.указывает на то, вызывается ли он в результате действия virtual деструктора.Хотя это, вероятно, будет иметь другие негативные последствия и, возможно, не очень хорошая идея (например, если есть случаи, когда operator delete вызывается без вызова деструктора).Но это сделало бы проблему до боли очевидной.

2 голосов
/ 14 января 2012

Предположительно, ваш последний бит кода призван сказать:

Base *b = new Derived;
delete b;  // delete b, not d.

В этом случае краткий ответ заключается в том, что он остается неопределенным поведением.Тот факт, что рассматриваемый класс или структура является POD, стандартным макетом или легко копируемым, ничего не меняет.

Да, вы передаете правильный адрес, и да, вы и я знаем, что в этомв случае, если dtor в значительной степени не nop - тем не менее, указатель, который вы передаете delete, имеет другой статический тип, чем динамический тип, и статический тип не имеет виртуального dtor.Стандарт совершенно ясно, что это дает неопределенное поведение.

С практической точки зрения, вы, вероятно, можете избежать неприятностей с UB, если вы действительно настаиваете - шансы довольно хороши, что не будет никаких вредных побочных эффектов от того, что вы делаете, по крайней мере, с большинствомТипичные компиляторы.Однако помните, что даже в лучшем случае код является чрезвычайно хрупким, поэтому, казалось бы, тривиальные изменения могут сломать все - и даже переключение на компилятор с очень тяжелой проверкой типов, и такие могут также сделать это.

Что касается вашего аргумента, ситуация довольно проста: в основном это означает, что комитет, вероятно, может сделать это определенное поведение, если они захотят.Однако, насколько мне известно, это никогда не предлагалось, и даже если бы оно имело место, это, вероятно, было бы элементом с очень низким приоритетом - он на самом деле не добавляет много, включает новые стили программирования и т. Д.

0 голосов
/ 14 января 2012

Существует много дискуссий по нерелевантным вопросам выше.Да, в основном для совместимости с С есть ряд гарантий, на которые вы можете положиться, если знаете, что делаете.Все это, однако, не имеет отношения к вашему основному вопросу.Главный вопрос: есть ли ситуация, когда объект может быть удален с использованием типа указателя, который не соответствует динамическому типу объекта, и где указанный тип не имеет виртуального деструктора.Ответ: нет, нет.

Логика для этого может быть получена из того, что система времени выполнения должна делать: она получает указатель на объект и получает запрос на его удаление.Необходимо будет хранить информацию о том, как вызывать деструкторы производного класса, или о количестве памяти, которое фактически занимает объект, если это будет определено.Тем не менее, это может повлечь за собой весьма существенные затраты с точки зрения использования памяти.Например, если первый элемент требует очень строгого выравнивания, например, чтобы быть выровненным по 8-байтовой границе, как в случае double, добавление размера добавило бы издержки по меньшей мере 8 байтов для выделения памяти.Даже если это звучит не так уж плохо, это может означать, что только один объект вместо двух или четырех помещается в строку кэша, что существенно снижает производительность.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...