[[no_unique_address]] e dois valores de membro do mesmo tipo

16

Eu estou brincando com [[no_unique_address]]no c++20.

No exemplo em cppreference , temos um tipo Emptye um tipo vaziosZ

struct Empty {}; // empty class

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

Aparentemente, o tamanho de Zdeve ser pelo menos 2porque os tipos e1e e2são iguais.

No entanto, eu realmente quero ter Zcom tamanho 1. Isso me fez pensar, que tal empacotar Emptyem alguma classe de empacotador com parâmetro extra de modelo que aplica diferentes tipos de e1e 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;
};

Infelizmente, sizeof(Z1)==2. Existe um truque para definir o tamanho de Z1um?

Estou testando isso com gcc version 9.2.1eclang version 9.0.0


No meu aplicativo, tenho muitos tipos vazios do formulário

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

Que é um tipo vazio se Te Stambém são tipos vazios e distintos! Quero que esse tipo fique vazio, mesmo que sejam Te Ssejam os mesmos.

tom
fonte
2
Que tal adicionar argumentos de modelo a Tsi mesmo? Isso geraria tipos distintos. Neste momento, o fato de que tanto Wrapperé herdam Testá prendendo você ...
Max Langhof
@MaxLanghof O que você quer dizer com adicionar um argumento de modelo T? No momento, Té um argumento de modelo.
Tom
Não herda de T.
Evg
@ Evg não faz diferença aqui.
eerorika
2
Só porque ele é maior do que 1 não torná-lo não vazia: coliru.stacked-crooked.com/a/51aa2be4aff4842e
Deduplicator

Respostas:

6

Que é um tipo vazio se Te Stambém são tipos vazios e distintos! Quero que esse tipo fique vazio, mesmo que sejam Te Ssejam os mesmos.

Você não pode entender isso. Tecnicamente falando, você não pode sequer garantir que vai ser esvaziar mesmo Te Sdiferentes tipos vazias. Lembre-se: no_unique_addressé um atributo; a capacidade de ocultar objetos é totalmente dependente da implementação. De uma perspectiva de padrões, você não pode impor o tamanho de objetos vazios.

À medida que as implementações do C ++ 20 amadurecem, você deve assumir que [[no_unique_address]]geralmente seguirá as regras de otimização da base vazia. Ou seja, desde que dois objetos do mesmo tipo não sejam subobjetos, é provável que você se esconda. Mas, neste momento, é uma espécie de sorte do pote.

Quanto ao caso específico de Te Ssendo do mesmo tipo, isso simplesmente não é possível. Apesar das implicações do nome "no_unique_address", a realidade é que o C ++ exige que, dados dois ponteiros para objetos do mesmo tipo, esses ponteiros apontem para o mesmo objeto ou tenham endereços diferentes. Eu chamo isso de "regra de identidade exclusiva" e no_unique_addressnão afeta isso. De [intro.object] / 9 :

Dois objetos com vida útil sobreposta que não são campos de bits podem ter o mesmo endereço se um estiver aninhado no outro ou se pelo menos um for um subobjeto de tamanho zero e forem de tipos diferentes ; caso contrário, eles têm endereços distintos e ocupam bytes de armazenamento separados.

Membros de tipos vazios declarados [[no_unique_address]]com tamanho zero, mas ter o mesmo tipo torna isso impossível.

De fato, pensar nisso, tentar ocultar o tipo vazio via aninhamento ainda viola a regra de identidade exclusiva. Considere o seu Wrappere Z1caso. Dado um z1exemplo de que é uma instância Z1, fica claro que z1.e1e z1.e2são objetos diferentes com tipos diferentes. No entanto, z1.e1não está aninhado z1.e2nem vice-versa. E enquanto eles têm tipos diferentes, (Empty&)z1.e1e não(Empty&)z1.e2 são tipos diferentes. Mas eles apontam para objetos diferentes.

E pela regra de identidade exclusiva, eles devem ter endereços diferentes. Assim, mesmo que e1e e2são nominalmente diferentes tipos, seus internos também devem identidade única obedecer contra outros subobjects no mesmo objeto que contém. Recursivamente.

O que você deseja é simplesmente impossível no C ++ como está atualmente, independentemente de como você tenta.

Nicol Bolas
fonte
Ótima explicação, muito obrigado!
tom
2

Até onde eu sei, isso não é possível se você quiser ter os dois membros. Mas você pode se especializar e ter apenas um dos membros quando o tipo for igual e vazio:

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; };
};

Obviamente, o restante do programa que usa os membros precisaria ser alterado para lidar com o caso em que há apenas um membro. Não importa qual membro é usado nesse caso - afinal, é um objeto sem estado sem endereço exclusivo. As funções de membro mostradas devem tornar isso simples.

infelizmente, sizeof(Empty<Empty<A,A>,A>{})==2onde A é uma estrutura completamente vazia.

Você pode introduzir mais especializações para oferecer suporte à compactação recursiva de pares vazios:

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; };
};

Ainda mais, para comprimir algo parecido 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
fonte
Isso é bom, mas ainda assim, infelizmente, sizeof(Empty<Empty<A,A>,A>{})==2onde Aé uma estrutura completamente vazio.
tom
Eu adicionaria uma get_empty<T>função. Em seguida, você pode reutilizar a get_empty<T>esquerda ou a direita, se ela já estiver funcionando lá.
Yakk - Adam Nevraumont