[[no_unique_address]] и два значения элемента одного типа

16

Я играю с [[no_unique_address]]в c++20.

В примере на cppreference у нас есть пустой тип Emptyи типZ

struct Empty {}; // empty class

struct Z {
    char c;
    [[no_unique_address]] Empty e1, e2;
};

Видимо, размер Zдолжен быть хотя бы 2потому, что типы e1и e2одинаковы.

Тем не менее, я действительно хочу иметь Zс размером 1. Это заставило меня задуматься, что насчет упаковки Emptyв некоторый класс-обертку с дополнительным параметром шаблона, который обеспечивает различные типы e1и e2.

template <typename T, int i>
struct Wrapper : public T{};

struct Z1 {
    char c;
    [[no_unique_address]] Wrapper<Empty,1> e1;
    [[no_unique_address]] Wrapper<Empty,2> e2;
};

К сожалению, sizeof(Z1)==2. Есть ли хитрость, чтобы сделать размер, Z1чтобы быть один?

Я проверяю это с gcc version 9.2.1иclang version 9.0.0


В моем приложении у меня есть много пустых типов формы

template <typename T, typename S>
struct Empty{
    [[no_unique_address]] T t;
    [[no_unique_address]] S s;
};

Который является пустым типом, если Tи Sтакже являются пустыми типами и отличными! Я хочу, чтобы этот тип был пустым, даже если Tи Sте же типы.

Том
источник
2
Как насчет добавления аргументов шаблона к Tсебе? Это будет генерировать различные типы. Прямо сейчас тот факт, что оба Wrapperунаследовали от Tвас, сдерживает вас ...
Макс Лангхоф
@MaxLanghof Что вы подразумеваете под добавлением аргумента шаблона T? Прямо сейчас, Tэто шаблон аргумента.
Том
Не наследуй от T.
Evg
@Evg здесь без разницы.
eerorika
2
То, что оно больше 1, не делает его непустым: coliru.stacked-crooked.com/a/51aa2be4aff4842e
Дедупликатор,

Ответы:

6

Который является пустым типом, если Tи Sтакже являются пустыми типами и отличными! Я хочу, чтобы этот тип был пустым, даже если Tи Sте же типы.

Вы не можете этого получить. Технически говоря, вы даже не можете гарантировать , что это будет пустой , даже если Tи Sразличные пустые типы. Помните: no_unique_addressэто атрибут; способность его скрывать объекты полностью зависит от реализации. С точки зрения стандартов, вы не можете установить размер пустых объектов.

По мере развития реализаций C ++ 20 вы должны предположить, что в [[no_unique_address]]целом они будут следовать правилам оптимизации пустой базы. А именно, если два объекта одного типа не являются подобъектами, вы, вероятно, можете ожидать скрытия. Но на данный момент это своего рода неудача.

Что касается конкретного случая Tи Sтого же типа, это просто невозможно. Несмотря на значение имени «no_unique_address», реальность такова, что C ++ требует, чтобы с учетом двух указателей на объекты одного типа эти указатели либо указывали на один и тот же объект, либо имели разные адреса. Я называю это «правилом уникальной идентичности», и no_unique_addressэто не влияет на это. Из [intro.object] / 9 :

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

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

Действительно, если подумать об этом, попытка скрыть пустой тип с помощью вложения все еще нарушает правило уникальной идентификации. Рассмотрим ваши Wrapperи Z1случай. Учитывая, z1что является экземпляром Z1, ясно, что z1.e1и z1.e2это разные объекты с разными типами. Однако,z1.e1 не не вложен z1.e2ни внутри, ни наоборот. И в то время как они имеют разные типы, (Empty&)z1.e1и (Empty&)z1.e2это не разные типы. Но они указывают на разные объекты.

И по уникальному правилу идентификации они должны иметь разные адреса. Так что , хотя e1и e2номинально различных типов, их внутренние также должны подчиняться уникальный идентификатор против других подобъектов в том же содержащей объект. Рекурсивный.

То, что вы хотите, просто невозможно в C ++ в его нынешнем виде, независимо от того, как вы пытаетесь.

Николь Болас
источник
Отличное объяснение, большое спасибо!
Том
2

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

template <typename T, typename S, typename = void>
struct Empty{
    [[no_unique_address]] T t;
    [[no_unique_address]] S s;

    constexpr T& get_t() noexcept { return t; };
    constexpr S& get_s() noexcept { return s; };
};

template<typename TS>
struct Empty<TS, TS, typename std::enable_if_t<std::is_empty_v<TS>>>{
    [[no_unique_address]] TS ts;

    constexpr TS& get_t() noexcept { return ts; };
    constexpr TS& get_s() noexcept { return ts; };
};

Конечно, остальная часть программы, которая использует участников, должна быть изменена, чтобы иметь дело со случаем, когда есть только один участник. Не имеет значения, какой элемент используется в этом случае - в конце концов, это объект без состояния без уникального адреса. Показанные функции-члены должны сделать это простым.

к сожалению, sizeof(Empty<Empty<A,A>,A>{})==2где A является совершенно пустой структурой.

Вы можете ввести больше специализаций для поддержки рекурсивного сжатия пустых пар:

template<class TS>
struct Empty<Empty<TS, TS>, TS, typename std::enable_if_t<std::is_empty_v<TS>>>{
    [[no_unique_address]] Empty<TS, TS> ts;

    constexpr Empty<TS, TS>& get_t() noexcept { return ts; };
    constexpr TS&            get_s() noexcept { return ts.get_s(); };
};

template<class TS>
struct Empty<TS, Empty<TS, TS>, typename std::enable_if_t<std::is_empty_v<TS>>>{
    [[no_unique_address]] Empty<TS, TS> ts;

    constexpr TS&            get_t() noexcept { return ts.get_t(); };
    constexpr Empty<TS, TS>& get_s() noexcept { return ts; };
};

Еще больше, чтобы сжать что-то вроде Empty<Empty<A, char>, A>.

template <typename T, typename S>
struct Empty<Empty<T, S>, S, typename std::enable_if_t<std::is_empty_v<S>>>{
     [[no_unique_address]] Empty<T, S> ts;

    constexpr Empty<T, S>& get_t() noexcept { return ts; };
    constexpr S&           get_s() noexcept { return ts.get_s(); };
};

template <typename T, typename S>
struct Empty<Empty<S, T>, S, typename std::enable_if_t<std::is_empty_v<S>>>{
     [[no_unique_address]] Empty<S, T> st;

    constexpr Empty<S, T>& get_t() noexcept { return st; };
    constexpr S&           get_s() noexcept { return st.get_t(); };
};


template <typename T, typename S>
struct Empty<T, Empty<T, S>, typename std::enable_if_t<std::is_empty_v<T>>>{
     [[no_unique_address]] Empty<T, S> ts;

    constexpr T&           get_t() noexcept { return ts.get_t(); };
    constexpr Empty<T, S>  get_s() noexcept { return ts; };
};

template <typename T, typename S>
struct Empty<T, Empty<S, T>, typename std::enable_if_t<std::is_empty_v<T>>>{
     [[no_unique_address]] Empty<S, T> st;

    constexpr T&           get_t() noexcept { return st.get_s(); };
    constexpr Empty<S, T>  get_s() noexcept { return st; };
};
eerorika
источник
Это хорошо, но все же, к сожалению, sizeof(Empty<Empty<A,A>,A>{})==2где Aполностью пустая структура.
Том
Я бы добавил get_empty<T>функцию. Затем вы можете повторно использовать get_empty<T>слева или справа, если он уже работает там.
Якк - Адам Невраумонт