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

16

Estoy jugando con [[no_unique_address]]adentro c++20.

En el ejemplo de cppreference tenemos un tipo Emptyy un tipo vacíosZ

struct Empty {}; // empty class

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

Aparentemente, el tamaño de Ztiene que ser al menos 2porque los tipos de e1y e2son iguales.

Sin embargo, realmente quiero tener Zcon el tamaño 1. Esto me hizo pensar, ¿qué pasa con el ajuste Emptyen alguna clase de contenedor con un parámetro de plantilla adicional que impone diferentes tipos de e1y 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;
};

Por desgracia, sizeof(Z1)==2. ¿Hay algún truco para hacer el tamaño de Z1ser uno?

Estoy probando esto con gcc version 9.2.1yclang version 9.0.0


En mi solicitud, tengo muchos tipos vacíos del formulario

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

¡Cuál es un tipo vacío si Ty Stambién son tipos vacíos y distintos! Quiero que este tipo esté vacío, incluso si Ty Sson los mismos tipos.

tom
fuente
2
¿Qué hay de agregar argumentos de plantilla a Tsí mismo? Eso generaría distintos tipos. En este momento, el hecho de que ambos Wrapperhereden te Testá frenando ...
Max Langhof
@MaxLanghof ¿Qué quieres decir con agregar un argumento de plantilla T? En este momento, Tes un argumento de plantilla.
Tom
No heredes de T.
Evg
@Evg no hace ninguna diferencia aquí.
eerorika
2
El hecho de que es más grande que 1 no significa que sea no vacía: coliru.stacked-crooked.com/a/51aa2be4aff4842e
Deduplicator

Respuestas:

6

¡Cuál es un tipo vacío si Ty Stambién son tipos vacíos y distintos! Quiero que este tipo esté vacío incluso siTy Sson los mismos tipos.

No puedes entender eso. Técnicamente hablando, ni siquiera se puede garantizar que va a estar vacío, incluso si Ty Sson diferentes tipos vacías. Recuerda:no_unique_address es un atributo; la capacidad de ocultar objetos depende completamente de la implementación. Desde una perspectiva estándar, no puede imponer el tamaño de los objetos vacíos.

A medida que las implementaciones de C ++ 20 maduren, 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 espere esconderse. Pero en este punto, es una especie de suerte.

En cuanto al caso específico Ty Sser 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. Yo llamo a esto la "regla de identidad única", y no_unique_addressno afecta eso. Desde [intro.object] / 9 :

Dos objetos con vidas superpuestas que no son campos de bits pueden tener la misma dirección si uno está anidado dentro del otro, o si al menos uno es un subobjeto de tamaño cero y son de diferentes tipos ; de lo contrario, tienen direcciones distintas y ocupan bytes de almacenamiento disjuntos.

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

De hecho, al pensar en ello, intentar ocultar el tipo vacío a través de la anidación todavía viola la regla de identidad única. Considere su caso Wrappery Z1. Dado z1que es una instancia de Z1, está claro que z1.e1y z1.e2son diferentes objetos con diferentes tipos. Sin embargo, z1.e1no está anidado dentro z1.e2ni viceversa. Y si bien tienen diferentes tipos, (Empty&)z1.e1y no(Empty&)z1.e2 son diferentes tipos. Pero sí señalan diferentes objetos.

Y por la regla de identidad única, deben tener diferentes direcciones. Así que aunquee1 y e2son nominalmente diferentes tipos, sus componentes internos también deben obedecer identidad única contra otros objetos parciales en el mismo objeto que contiene. Recursivamente.

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

Nicol Bolas
fuente
Gran explicación, muchas gracias!
Tom
2

Por lo que puedo decir, 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, el resto del programa que usa los miembros necesitaría ser cambiado para tratar el caso donde 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 mostradas deberían simplificar eso.

desafortunadamente sizeof(Empty<Empty<A,A>,A>{})==2donde A es una estructura completamente vacía.

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, comprimir algo así 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
fuente
Esto es bueno, pero por desgracia aún sizeof(Empty<Empty<A,A>,A>{})==2cuando Aes una estructura completamente vacío.
Tom
Yo agregaría una get_empty<T>función. Luego, puede volver a usarlo a la get_empty<T>izquierda o derecha si ya funciona allí.
Yakk - Adam Nevraumont