[[no_unique_address]] y dos valores de miembro del mismo tipo

[[no_unique_address]] y dos valores de miembro del mismo tipo

No puedes conseguir eso. Técnicamente hablando, ni siquiera puedes garantizar que estará vacío incluso si T y S son diferentes tipos vacíos. Recuerda:no_unique_address es un atributo; la capacidad de ocultar objetos es totalmente dependiente de la implementación. Desde la perspectiva de los estándares, no puede imponer el tamaño de los objetos vacíos.

A medida que maduran las implementaciones de C++20, debe suponer que [[no_unique_address]] generalmente seguirá las reglas de optimización de base vacía. Es decir, siempre que dos objetos del mismo tipo no sean subobjetos, es probable que se esconda. Pero en este punto, es una especie de comida compartida.

En cuanto al caso concreto de T y S siendo del mismo tipo, eso simplemente no es posible. A pesar de las implicaciones del nombre "no_unique_address", la realidad es que C++ requiere que, dados dos punteros a objetos del mismo tipo, esos punteros apunten al mismo objeto o tengan direcciones diferentes. Llamo a esto la "regla de identidad única", y no_unique_address eso no afecta. De [intro.objeto]/9:

Miembros de tipos vacíos declarados como [[no_unique_address]] son de tamaño cero, pero tener el mismo tipo hace que esto sea imposible.

De hecho, pensándolo bien, intentar ocultar el tipo vacío mediante el anidamiento aún viola la regla de identidad única. Considere su Wrapper y Z1 caso. Dado un z1 que es una instancia de Z1 , está claro que z1.e1 y z1.e2 son diferentes objetos con diferentes tipos. Sin embargo, z1.e1 no está anidado dentro de z1.e2 ni viceversa. Y aunque tienen diferentes tipos, (Empty&)z1.e1 y (Empty&)z1.e2 son no diferentes tipos. Pero apuntan a diferentes objetos.

Y según la regla de identidad única, deben tienen diferentes direcciones. Entonces, aunque e1 y e2 son tipos nominalmente diferentes, sus partes internas también deben obedecer a una identidad única frente a otros subobjetos en el mismo objeto que los contiene. De forma recursiva.

Lo que desea es simplemente imposible en C++ tal como está actualmente, independientemente de cómo lo intente.


Por lo que sé, eso no es posible si quieres tener ambos miembros. Pero puede especializarse y tener solo uno de los miembros cuando el tipo es el mismo y está vacío:

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

Por supuesto, sería necesario cambiar el resto del programa que usa los miembros para tratar el caso en el que solo hay un miembro. No debería importar qué miembro se usa en este caso; después de todo, es un objeto sin estado sin una dirección única. Las funciones miembro que se muestran deberían hacerlo simple.

Podría introducir más especializaciones para admitir la compresión recursiva de pares vacíos:

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

Aún más, para comprimir algo como 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; };
};