На что влияет атрибут [[no_unique_address]] у нестатического поля и какой компромисс он вносит?
[[no_unique_address]] разрешает компилятору не выделять полю уникальный адрес и размещать его в уже существующем пространстве объекта, включая свободное выравнивающее заполнение. Чаще всего это уменьшает размер класса с пустым объектом-политикой, но не гарантирует конкретный размер объекта или фактическое совмещение адресов.
Главный компромисс — потеря гарантии уникального адреса такого поля. Код не должен использовать адрес атрибутированного объекта как уникальный идентификатор.
Пустой класс всё равно обычно занимает ненулевой размер как поле: разные объекты должны иметь различимые адреса. Для базовых классов давно применялась оптимизация пустого базового класса, но при композиции через обычное поле аналогичная оптимизация не была стандартным механизмом.
В C++20 атрибут сделал такую оптимизацию доступной непосредственно для нестатических полей и позволил сохранять композицию вместо искусственного наследования ради уменьшения размера объекта.
Обобщённый класс часто хранит объект политики: компаратор, аллокатор, deleter или тип-тег. Такая политика может не содержать данных, однако обычное поле всё равно способно увеличивать размер каждого экземпляра и ухудшать размещение элементов в контейнерах.
Неверно считать атрибут приказом «сделать поле нулевого размера». Реализация лишь получает разрешение на перекрытие; она может сохранить обычное размещение. Кроме того, после оптимизации адрес поля может совпасть с адресом другого подобъекта или поля.
Атрибут применяется к нестатическому полю и сообщает, что этому полю не обязательно иметь уникальный адрес. Для пустого типа компилятор может разместить поле в padding, созданном выравниванием другого поля, либо совместить его адрес с подходящим участком объекта.
Результат зависит от реализации, ABI и выравнивания. Важно не ожидать конкретных чисел: атрибут разрешает оптимизацию, но не требует её. Поле сохраняет обычные свойства члена класса: его можно инициализировать, читать и вызывать его методы, а его время жизни не исчезает.
Атрибут особенно полезен для пустых policy-типа. Для непустого типа он также может позволить использовать padding, но выигрыш не гарантирован. В отличие от наследования, решение явно выражает композицию, однако layout публичного класса всё равно остаётся зависимым от реализации, поэтому изменение атрибута может нарушить ABI.
В библиотечном контейнере хранились буфер и пустой объект политики. Обычное поле добавляло размер к каждому контейнеру, а при хранении миллионов объектов это увеличивало расход памяти и ухудшало кэш-локальность.
Рассматривались два варианта. Наследование могло задействовать оптимизацию пустого базового класса, но усложняло модель типов, доступ и правила для нескольких политик. Обычное поле было проще и предсказуемее по интерфейсу, но не давало возможности сжать layout.
Выбран был [[no_unique_address]] на поле политики. Это сохранило композицию и позволило реализациям уменьшить размер объекта. При этом в документации явно запретили использовать адрес политики для идентификации, а размер и бинарный layout не считали переносимыми между ABI.
1. Гарантирует ли атрибут, что поле не увеличит размер объекта?
Нет. Он только разрешает реализацию размещать поле без отдельного пространства. Компилятор может проигнорировать потенциальную выгоду из-за выравнивания, ограничений layout или особенностей ABI, поэтому переносимый код не должен строить логику на конкретном sizeof.
2. Можно ли сравнивать адрес такого поля с адресами других полей и считать адрес уникальным?
Нет. У атрибутированного поля может совпасть адрес с другим подобъектом или областью, использованной для хранения другого поля. Поэтому адрес нельзя надёжно применять как ключ, токен идентичности или доказательство того, что переданы разные объекты.
3. Полностью ли [[no_unique_address]] заменяет оптимизацию пустого базового класса?
Нет. Атрибут работает для нестатического поля и позволяет сохранить композицию, тогда как оптимизация пустого базового класса относится к базовым подобъектам и имеет собственные правила. Они решают близкую задачу разными средствами; выбор зависит от требуемой модели типов, layout, ABI и ограничений конкретного класса.