Для какого члена требуется no_unique_address и почему?

Рассмотрим следующие два 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;
};

Почему?

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

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

NathanOliver 22.07.2024 17:13

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

BeeOnRope 22.07.2024 17:14
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
2
109
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Чтобы понять, почему это идет по первому члену, нужно понять причину, по которой компилятору запрещено делать это без явной разметки. Одна конкретная причина указана в [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.

Другие вопросы по теме