Что предотвращает перекрытие смежных членов в классах?

12

Рассмотрим следующие три structс:

class blub {
    int i;
    char c;

    blub(const blub&) {}
};

class blob {
    char s;

    blob(const blob&) {}
};

struct bla {
    blub b0;
    blob b1;
};

На типовых платформах, где int4 байта, размеры, выравнивания и общее заполнение 1 следующие:

  struct   size   alignment   padding  
 -------- ------ ----------- --------- 
  blub        8           4         3  
  blob        1           1         0  
  bla        12           4         6  

Не существует дублирования между хранением элементов blubи blob, даже если размер 1 blobв принципе может «вписаться» в отступы blub.

C ++ 20 вводит no_unique_addressатрибут, который позволяет соседним пустым элементам использовать один и тот же адрес. Это также явно позволяет описанный выше сценарий использования заполнения одного элемента для хранения другого. Из контекста (акцент мой):

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

В самом деле, если мы используем этот атрибут blub b0, размер blaпадает до 8, так что blobон действительно сохраняется в том blub виде, в каком это видно на Годболте .

Наконец, мы добрались до моего вопроса:

Какой текст в стандартах (C ++ 11 - C ++ 20) предотвращает это перекрытие no_unique_address, для объектов, которые нетривиально копируются?

Мне нужно исключить тривиально копируемые (TC) объекты из вышеперечисленного, потому что для объектов TC разрешено переходить std::memcpyот одного объекта к другому, включая дочерние подобъекты, и если хранилище было перекрыто, это сломалось бы (потому что все или часть хранилища для соседнего члена будет перезаписано) 2 .


1 Мы вычисляем заполнение просто как разницу между размером структуры и размером всех составляющих ее элементов, рекурсивно.

2 Вот почему я определил конструкторы копирования: сделать blubи blobне просто копировать .

BeeOnRope
источник
Я не исследовал это, но я предполагаю правило «как будто». Если нет заметной разницы (термин с очень специфическим значением между прочим) для абстрактной машины (против чего компилируется ваш код), то компилятор может изменить код так, как ему нравится.
Джеспер Юл
Уверен, что это
обманка
@JesperJuhl - верно, но я спрашиваю, почему не может , а не почему , а правило «как будто» обычно применяется к первому, но не имеет смысла для второго. Кроме того, «как будто» не ясно для структуры структуры, которая обычно является глобальной проблемой, а не локальной. В конечном итоге компилятор должен иметь единый согласованный набор правил для разметки, за исключением, возможно, структур, которые он может никогда не «убежать».
BeeOnRope
1
@BeeOnRope Я не могу ответить на ваш вопрос, извините. Вот почему я только что оставил комментарий, а не ответ. То, что вы получили в этом комментарии, было моей лучшей догадкой к объяснению, но я не знаю ответа (самодовольно выучить его - вот почему вы получили голос).
Джеспер Юл
1
@NicolBolas - вы отвечаете на правильный вопрос? Это не об обнаружении безопасных копий или чего-либо еще. Скорее мне любопытно, почему отступы не могут быть повторно использованы между участниками. В любом случае вы ошибаетесь: тривиально копируемое свойство является свойством типа и всегда имело место. Тем не менее, для безопасного копирования объекта он должен как иметь тип ТС (свойство типа), а не быть потенциально перекрывающихся объекта съемки (свойство объекта, который я предполагаю, где вы запутались). Все еще не знаю, почему мы говорим о копиях здесь, хотя.
BeeOnRope

Ответы:

1

Стандарт очень тихий, когда речь идет о модели памяти, и не очень четко описывает некоторые термины, которые он использует. Но я думаю, что нашел рабочую аргументацию (которая может быть немного слабой)

Во-первых, давайте выясним, что является частью объекта. [basic.types] / 4 :

Объектное представление объекта типа T- это последовательность N unsigned charобъектов, занятых объектом типа T, где Nравно sizeof(T). Представление значения объекта типа T- это набор битов, которые участвуют в представлении значения типа T. Биты в представлении объекта, которые не являются частью представления значения, являются битами заполнения.

Таким образом, объектное представление b0состоит изsizeof(blub) unsigned char объектов, поэтому 8 байтов. Биты заполнения являются частью объекта.

Ни один объект не может занимать пространство другого, если он не является вложенным в него [basic.life] /1.5 :

Жизни объекта oтипа Tзаканчивается , когда:

[...]

(1.5) хранилище, которое занимает объект, освобождается или используется объектом, который не вложен в o([intro.object]).

Таким образом, время жизни b0может закончиться, когда занимаемое им хранилище будет повторно использовано другим объектом, т.е.b1 . Я не проверял это, но я думаю, что стандарт требует, чтобы подобъект живого объекта также был живым (и я не мог представить, как это должно работать по-другому).

Так хранение , что b0 занимает не может быть использовано b1. Я не нашел определения «занимают» в стандарте, но я думаю, что разумная интерпретация будет «частью представления объекта». В представлении описания объекта цитаты используются слова «занять» 1 . Здесь это будет 8 байт, поэтому blaнужен как минимум еще один дляb1 .

Специально для подобъектов (так что среди прочих нестатических элементов данных) есть также условие [intro.object] / 9 (но это было добавлено в C ++ 20, thx @BeeOnRope)

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

(выделение мое) Здесь снова у нас есть проблема, которая «не занимает» не определена, и снова я бы поспорил взять байты в представлении объекта. Обратите внимание, что есть сноска к этому [basic.memobj] / footnote 29

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

Что может позволить компилятору нарушить это, если он может доказать, что наблюдаемого побочного эффекта нет. Я думаю, что это довольно сложно для такой фундаментальной вещи, как макет объекта. Возможно, именно поэтому эта оптимизация выполняется только тогда, когда пользователь предоставляет информацию о том, что нет причин создавать непересекающиеся объекты путем добавления[no_unique_address] атрибута.

tl; dr: заполнение может быть частью объекта, и члены должны быть непересекающимися.


1 Я не мог удержаться от добавления ссылки, которая занимает, может означать, чтобы заняться: Пересмотренный словарь Вебстера, G. & C. Merriam, 1913 (выделено мной)

  1. Для хранения или заполнения размеров; занять комнату; покрыть или заполнить; как лагерь занимает пять акров земли. Сэр Дж. Гершель.

Какой стандартный обход будет обходиться без сканирования словаря?

n314159
источник
2
Я думаю, мне будет достаточно части «занять непересекающиеся байты памяти» из into.storage, но эта формулировка была добавлена ​​только в C ++ 20 как часть внесенных изменений no_unique_address. Это оставляет ситуацию до C ++ 20 менее ясной. Я не понял ваших рассуждений, приводящих к тому, что «ни один объект не может занимать пространство другого, если он не является вложенным в него» из basic.life/1.5, в частности, как получить из «хранилища, которое занимает объект, освобождено» «ни один объект не может занимать пространство другого».
BeeOnRope
1
Я добавил небольшое пояснение к этому пункту. Я надеюсь, что это делает это более понятным. В противном случае я посмотрю это снова завтра, сейчас уже довольно поздно для меня.
n314159
«Два объекта с перекрывающимися временами жизни, которые не являются битовыми полями, могут иметь один и тот же адрес, если один вложен в другой, или если по крайней мере один является подобъектом нулевого размера, и они имеют разные типы» 2 объекта с перекрывающимися временами жизни, того же типа, имеют тот же адрес .
Языковой адвокат
Извините, не могли бы вы уточнить? Вы цитируете стандартную цитату из моего ответа и приводите пример, который немного противоречит этому. Я не уверен, если это комментарий к моему ответу, и если это то, что он должен сказать мне. Что касается вашего примера, я бы сказал, что нужно рассмотреть еще другие части стандарта (есть параграф о массиве символов без знака, обеспечивающем хранение для другого объекта, что-то относительно оптимизации базы нулевого размера, и еще больше следует также посмотреть, имеет ли место размещение new специальные пособия, все вещи, которые я не считаю относящимися к примеру ОП)
n314159
@ n314159 Думаю, эта формулировка неверна.
Языковой адвокат