Рассмотрим следующие два struct
, размеры которых составляют 8 и 1 байт соответственно:
class eight {
int i;
char c;
eight(const blub&) {}
};
class one {
char s;
one(const blob&) {}
};
Когда они встроены в другую структуру, например:
struct example1 {
eight b0;
one b1;
};
sizeof(example1)
будет иметь размер 12 байт, потому что b0
и b1
хранятся в непересекающихся хранилищах (9 байт), а затем требование 4-байтового выравнивания eight
округляет это число до 12. Посмотрите это на godbolt.
В C++20 представлен атрибут no_unique_address
, который позволяет соседним пустым членам использовать один и тот же адрес. Это также явно допускает описанный выше сценарий использования заполнения одного элемента для хранения другого. Из cppreference:
Указывает, что этот элемент данных не обязательно должен иметь адрес, отличный от всех других нестатических членов данных его класса. Это означает, что если член имеет пустой тип (например, распределитель без сохранения состояния), компилятор может оптимизировать его, чтобы он не занимал места, как если бы это была пустая база. Если элемент не пуст, любое заполнение хвоста в нем также может быть повторно использовано для хранения других элементов данных.
Если мы используем [[no_unique_address]]
для первого члена (eight
), размер уменьшится до 8 байт.
struct example2 {
[[no_unique_address]] eight b0;
one b1;
};
Однако, если мы поместим его на второй член, размер все равно составит 12 байт (см. на godbolt):
struct example3 {
eight b0;
[[no_unique_address]] one b1;
};
Почему?
Интуитивно я ожидаю, что это потребуется либо для обоих («оба перекрывающихся участника должны дать согласие»), либо для любого из них («любой может принять участие, тогда разрешено перекрытие»), но не для того, чтобы он работал на одном, а не на другой.
Я думаю, вы можете привести лошадь (меня) к воде, но не можете заставить ее пить (внимательно прочитайте документ, который они сами цитировали).
Чтобы понять, почему это идет по первому члену, нужно понять причину, по которой компилятору запрещено делать это без явной разметки. Одна конкретная причина указана в [basic.types]/2&3, где описывается возможность выполнения memcpy из одного тривиально копируемого объекта в другой того же типа. Эта копия действует точно так же, как копирование этих объектов:
Для любого объекта (кроме потенциально перекрывающегося подобъекта) тривиально копируемого типа T, независимо от того, объект содержит допустимое значение типа T, базовые байты (6.7.1), составляющие объект, могут быть скопированы в массив char, unsigned char или std::byte (17.2.1).36 Если содержимое этого массива копируется обратно в объект, объект впоследствии сохранит свое первоначальное значение.
Для любого тривиально копируемого типа T, если два указателя на T указывают на разные T объекты obj1 и obj2, где ни один obj1 и obj2 не являются потенциально перекрывающимися подобъектами, если базовые байты (6.7.1), составляющие obj1, скопированный в obj2,37 obj2 впоследствии будет содержать то же значение, что и obj1.
Это не позволяет компилятору b1
занять место в b0
. Почему? Потому что если бы это было так, то выполнение memcpy в b0
изменило бы b1
. В приведенном выше разделе нет ничего, что позволяло бы такой memcpy влиять на объекты, отличные от копируемых. Таким образом, компилятору запрещено разрешать b1
занимать место внутри b0
.
Но учтите, что копирование в b1
— это нормально. В этой операции memcpy нет ничего плохого, даже если хранилище b1
находится внутри b0
.
Возможно, вы заметили, что в цитируемом тексте предусмотрены исключения для «потенциально перекрывающихся подобъектов». Если поставить no_unique_address
к переменной-члену, она станет «потенциально перекрывающимся подобъектом». Это приводит к тому, что выполнение вышеуказанной операции memcpy приводит к неопределенному поведению. И поэтому теперь компилятор может использовать хранилище b0
для b1
.
Вот почему этот атрибут продолжается b0
: потому что именно b0
необходимо предотвратить использование определенными способами, а не b1
.
Вы выделили соответствующую часть жирным шрифтом. Если элемент не пуст, любое заполнение хвоста в нем также может быть повторно использовано для хранения других элементов данных. Это означает, что атрибут нужен предыдущему члену, чтобы вы могли использовать его пространство заполнения.