Что предотвращает перекрытие смежных членов в классах? - PullRequest
12 голосов
/ 22 января 2020

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

class blub {
    int i;
    char c;

    blub(const blub&) {}
};

class blob {
    char s;

    blob(const blob&) {}
};

struct bla {
    blub b0;
    blob b1;
};

На типовых платформах, где int составляет 4 байта, размеры, выравнивания и общее заполнение 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, который позволяет соседним пустым элементам использовать один и тот же адрес. Это также явно позволяет описанный выше сценарий использования заполнения одного элемента для хранения другого. From cppreference (выделено мной):

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

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

Наконец, мы подошли к моему вопросу:

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

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


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

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

1 Ответ

1 голос
/ 23 января 2020

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

Сначала давайте выясним, что является даже частью объекта. [basi c .types] / 4 :

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

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

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

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

[...]

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

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

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

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

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

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

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

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

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


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

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

Какой стандартный обход был бы полным без обхода по словарю?

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