«нет базовых классов того же типа, что и первый нестатический элемент данных» - PullRequest
5 голосов
/ 11 октября 2010

Я недавно спросил об этом на comp.std.c ++ и не получил ответа.

Я просто процитирую там мой пост с небольшой модификацией.


Является ли последнее требование стандартов классов 9/6 необходимым или полезным?

Предоставлено сноска:

Это гарантирует, что два подобъекта, которые имеют тот же тип класса и принадлежат к тому же самому производному объекту не размещены по одному адресу (5,10).

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

struct A {};
struct B : A {};
struct C : A {};
struct D : B, C {};

D d;
static_cast<A*>(static_cast<B*>(&d))
   == static_cast<A*>(static_cast<C*>(&d)); // allowed per 1.8/5

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

В C ++ 03 такого ограничения не было. Даже если есть ABI, который требует от каждого члена размещены по другому адресу из любой базы того же типа, но уже позволяет пустую оптимизацию базового класса по приведенному выше коду, я думаю, что ABI глючит, и стандарт не должен это фиксировать.

Язык восходит к N2172 что предполагает, что множественное наследование может вызвать проблемы и необходимость быть запрещенным в классах стандартной компоновки для обеспечения совместимости ABI ; однако в конечном итоге это было разрешено, и в этом свете требование не имеет смысла.


Для справки: 1,8 / 5-6:

5 Если это не битовое поле (9.6), наиболее производный объект должен иметь ненулевой размер и должен занимать один или больше байтов памяти. Базовый класс подобъекты могут иметь нулевой размер. объект тривиально копируемый или типовой макет (3.9) должен занимают смежные байты памяти.

6 Если объект не является битовым полем или подобъект базового класса нулевого размера, адрес этого объекта является адресом первого байта он занимает. Два отдельные объекты, которые не являются ни битовые поля или подобъекты базового класса нулевого размера должен иметь адреса.

(сноска) В соответствии с правилом «как если бы» реализация может хранить два объекта с одним и тем же машинным адресом или не сохранять объект вообще, если программа не может наблюдать разницу.

Дополнительные примечания:

10.1 / 8 относится к тому же таинственному содержанию в 5.10, но это также просто информативная заметка.

[Примечание:… подобъект базового класса может иметь нулевой размер (раздел 9); однако два подобъекта, которые имеют один и тот же тип класса и принадлежат к одному и тому же самому производному объекту, не должны размещаться по одному и тому же адресу (5.10). - конец примечания]

Похоже, что GCC гарантирует, что пустые базовые подобъекты одного типа получают уникальные адреса. Пример программы и выходных данных. Этого достаточно, чтобы гарантировать, что объекты данного типа однозначно идентифицируются по адресу. Это было бы сверх гарантий объектной модели C ++, §1.8. Конечно, это хорошая идея, но она не требуется Стандартом. Аналогично, платформа ABI может распространить эту гарантию на класс, в котором первый член имеет псевдоним пустой базы. Язык устанавливает минимальные требования для ABI; ABI может добавить функцию языка, а другие ABI могут последовать его примеру, и процесс наверстывания по Стандарту просто подвержен ошибкам.

Мой вопрос заключается в том, выполняет ли данное требование что-либо в контексте Стандарта, а не является ли оно полезным для пользователя в сочетании с другими гарантиями ABI. Свидетельство того, что такая гарантия с уникальным адресом была предназначена и опущена только случайно, также сделало бы это требование более значимым.


Чтобы подвести итог ответа (или, во всяком случае, моего заключения):

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

На практике требование позволяет реализациям продолжать использовать существующие ABI, если они включают пустую оптимизацию базового класса. Существующие компиляторы могут отключить EBO, когда требование нарушено, чтобы избежать совпадения адреса базы с адресом первого члена. Если бы Стандарт не ограничивал программы таким образом, библиотеки и программы пришлось бы перекомпилировать с обновленными компиляторами C ++ 0x… не стоит!

Ответы [ 3 ]

3 голосов
/ 11 октября 2010

Если вы поместите это выражение равенства в assert(), вы обнаружите, что оно терпит неудачу.Подобъекты A находятся в разных местах.Это правильное поведение без указания virtual:

struct B : virtual A {};
struct C : virtual A {};

При virtual D, по второму правилу, уже не является классом стандартной компоновки.Это имеет место в C ++ '98, '03 и '0x.

Изменить, чтобы отразить комментарии:

Редактировать еще раз: Неважно, этого недостаточно.

Смысл определения класса стандартной компоновки состоит в том, чтобы указать что-то, что можно использовать с другими языками.Давайте использовать C в качестве примера.В общем случае следующий класс C ++

struct X : public B{
  B b;
  int i;
};

будет эквивалентен этой структуре C:

struct X{
  B base;
  B b;
  int i;
};

Если бы B был пустым классом и применялась оптимизация с пустым основанием,X будет эквивалентен этому в C:

struct X{
  B b;
  int i;
};

Но сторона взаимодействия C не узнает об этой оптимизации.Экземпляры C ++ X и CX будут несовместимы.Ограничение предотвращает этот сценарий.

3 голосов
/ 11 октября 2010

Одна из «специальных способностей» класса стандартной компоновки заключается в том, что вы можете reinterpret_cast указатель на объект класса стандартной компоновки на тип его первого члена данных и, таким образом, получить указатель на первый элемент данных. [Редактировать: 9.2 / 19] Кроме того, классу стандартной компоновки с нестатическими элементами данных разрешено иметь пустые базы. Как вы, несомненно, знаете, большинство реализаций помещают подобъекты базового класса в начало завершенных подобъектов. Эта комбинация ограничений фактически обязывает , что оптимизация пустого базового класса применяется ко всем базам классов стандартной компоновки.

Однако, как объяснили другие ответы, все подобъекты базового класса и дочерние подобъекты, которые являются частью одного и того же законченного объекта, должны различаться, т. Е. Иметь разные адреса, если они одного типа. Классы, которые нарушают вашу точку маркера (имеют базовый класс того же типа, что и первый член), не могут полностью применять оптимизацию пустого базового класса и, следовательно, не могут быть классами стандартной компоновки, если базовые классы расположены в начале всего объекта.

Так что я почти уверен, что это то, к чему он стремится - он пытается сказать, "если у класса есть базовые классы, и оптимизация пустого базового класса не может быть применена, тогда класс не является стандартным макетом ».

Edit: я немного ослаблен с терминологией здесь - возможно построить случаи, когда пустая оптимизация базового класса не может быть полностью применена среди базовых классов (например, в вашем struct D), но это не имеет значения, потому что базовые классы все еще могут начинаться в начале объекта и концептуально «перекрывать» элементы данных, подобно union. Как вы говорите, базовые подобъекты получают свои адреса увеличенными, если они (или база) иначе перекрывают другую базу. Хотя то же самое может произойти с базами стандартных вариантов размещения (если они будут перекрывать элемент данных одного типа), это нарушит существующие ABI и добавит специальный случай для небольшого усиления.


Вы говорите, что это «запрещает» возможность - на самом деле это не запрещает, с моей точки зрения, это просто не предоставляет статус «стандартная раскладка» типам, которые в любом случае не имели этого (классы с базы не были POD в C ++ 03). Так что это не запрещает такие типы, это просто говорит о том, что они не получают специальной обработки стандартного макета, которую они не гарантировали в первую очередь.


Относительно моего утверждения о том, что субобъекты нестатических элементов данных и базовые подобъекты различны, посмотрите, найдете ли вы это убедительным:

  • 5.9 / 2 (реляционные операторы на указателях) проясняют, что никакие два подобъекта-члена данных (по крайней мере, с одним и тем же спецификатором доступа) не имеют одинаковый адрес друг с другом.
  • 5.3.1 / 1 (унарный оператор *) говорит, что «выражение, к которому оно применяется, должно быть указателем на тип объекта [snip], а результатом является lvalue, ссылающееся на объект на что указывает выражение ". (выделение добавлено) Это подразумевает, что в определенный момент времени в конкретный адрес входит не более одного объекта данного типа.
  • 1.8 / 2 «Субобъект может быть подобъектом-членом (9.2), подобъектом базового класса (пункт 10) или элементом массива.» ... Я думаю, это подразумевает, что категории являются взаимоисключающими (даже если их хранение перекрывается). Другие части стандарта довольно сильно подразумевают, что базовые подобъекты и подобъекты-члены различны (например, 12.6.2).
  • Цитата Стива М 10.1 / 4 "Для каждого отдельного вхождения не виртуального базового класса в решетке классов наиболее производного класса наиболее производный объект (1.8) должен содержат соответствующий отдельный подобъект базового класса этого типа. "Я считаю, что это означает, что разные базы должны быть по разным адресам, иначе они не будут" разными "объектами - не будет никакого способа различить их в течение их общего времени жизни.

Я не знаю, насколько это убедительно, если вы не считаете сноски нормативными или достаточно указывающими намерение. Для чего стоит, Страуструп объясняет производные классы в «Языке программирования C ++» 12.2 с точки зрения объектов-членов, которые имеют поддерживаемое компилятором преобразование из производного в базовое. Действительно, в самом конце этого раздела он явно говорит: «Использование класса в качестве базы эквивалентно объявлению (неназванного) объекта этого класса. Следовательно, класс должен быть определен для использования в качестве базы ( раздел 5.7). "


Кроме того: похоже, что GCC 4.5 не увеличивает базовый класс в этой конкретной ситуации, даже если он увеличивает базы, где вы повторяли базовые классы (как Вы показали):

#include <assert.h>
#include <iostream>

struct E {};
struct D: E { E x ; };

int main()
{
   D d;
   std::cerr << "&d: " << (void*)(&d) << "\n";
   std::cerr << "&d.x: " << (void*)(&(d.x)) << "\n";
   std::cerr << "(E*)&d: " << (void*)(E*)(&d) << "\n";
   assert(reinterpret_cast<E *>(&d) == &d.x); //standard-layout requirement
}

Вывод (Linux x86-64, GCC 4.5.0):

&d: 0x7fffc76c9420
&d.x: 0x7fffc76c9421
(E*)&d: 0x7fffc76c9420
testLayout: testLayout.cpp:19: int main(): Assertion `reinterpret_cast(&d) == &d.x' failed.
Aborted
2 голосов
/ 11 октября 2010

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

Я так не думаю. Фактически быстрая проверка с моей копией g ++ показывает, что у меня есть два различных адреса объекта A. То есть Ваш код выше не соответствует действительности.

Дело в том, что у нас должно быть два объекта A по тому, как написаны ваши классы. Если два объекта имеют один и тот же адрес, они не являются двумя разными объектами в каком-либо значимом смысле Таким образом, требуется, чтобы существовали разные адреса для экземпляров объекта А.

Предположим, что A определено так:

class A
{
   static std::set<A*> instances;
   A() { instances.insert(this); }
   ~A() { instances.remove(this); }
}

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

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