[[no_unique_address]] et deux valeurs de membre du même type

16

Je joue autour avec [[no_unique_address]]dans c++20.

Dans l'exemple sur cppreference, nous avons un type Emptyet un type videsZ

struct Empty {}; // empty class

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

Apparemment, la taille de Zdoit être au moins 2parce que les types de e1et e2sont les mêmes.

Cependant, je veux vraiment avoir Zavec la taille 1. Cela m'a fait réfléchir, qu'en est-il Emptyde l'encapsulation dans une classe wrapper avec un paramètre de modèle supplémentaire qui applique différents types de e1et 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;
};

Malheureusement sizeof(Z1)==2. Y a-t-il une astuce pour que la taille Z1soit de un?

Je teste cela avec gcc version 9.2.1etclang version 9.0.0


Dans ma demande, j'ai beaucoup de types vides du formulaire

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

Qui est un type vide si Tet Ssont également des types vides et distincts! Je veux que ce type à vide , même si Tet Ssont les mêmes types.

à M
la source
2
Qu'en est-il de l'ajout d'arguments de modèle à Tlui-même? Cela générerait des types distincts. En ce moment, le fait que les deux Wrapperhéritent de Tvous retient ...
Max Langhof
@MaxLanghof Que voulez-vous dire par l'ajout d'un argument de modèle T? À l'heure actuelle, Test un argument de modèle.
tom
N'héritez pas de T.
Evg
@Evg ne fait aucune différence ici.
eerorika
2
Ce n'est pas parce qu'il est plus grand que 1 qu'il n'est pas vide: coliru.stacked-crooked.com/a/51aa2be4aff4842e
Deduplicator

Réponses:

6

Qui est un type vide si Tet Ssont également des types vides et distincts! Je veux que ce type à vide , même si Tet Ssont les mêmes types.

Vous ne pouvez pas comprendre cela. Techniquement parlant, vous ne pouvez même pas garantir qu'il sera vide même si Tet Ssont différents types vides. Rappelez-vous: no_unique_addressest un attribut; sa capacité à masquer des objets dépend entièrement de l' implémentation. Du point de vue des normes, vous ne pouvez pas appliquer la taille des objets vides.

À mesure que les implémentations C ++ 20 arrivent à maturité, vous devez supposer qu'elles [[no_unique_address]]suivront généralement les règles d'optimisation de la base vide. À savoir, tant que deux objets du même type ne sont pas des sous-objets, vous pouvez probablement vous attendre à vous cacher. Mais à ce stade, c'est une sorte de chance.

En ce qui concerne le cas spécifique Tet Sle même type, ce n'est tout simplement pas possible. Malgré les implications du nom "no_unique_address", la réalité est que C ++ exige que, étant donné deux pointeurs vers des objets du même type, ces pointeurs pointent vers le même objet ou ont des adresses différentes. J'appelle cela la "règle d'identité unique", et no_unique_addresscela n'affecte pas cela. Depuis [intro.object] / 9 :

Deux objets dont les durées de vie se chevauchent et qui ne sont pas des champs binaires peuvent avoir la même adresse si l'un est imbriqué dans l'autre, ou si au moins l'un est un sous-objet de taille nulle et ils sont de types différents ; sinon, ils ont des adresses distinctes et occupent des octets de stockage disjoints.

Les membres de types vides déclarés comme [[no_unique_address]]étant de taille nulle, mais avoir le même type rendent cela impossible.

En effet, y penser, tenter de masquer le type vide via l'imbrication viole toujours la règle d'identité unique. Considérez votre Wrapperet votre Z1cas. Étant donné un z1qui est une instance de Z1, il est clair que z1.e1et z1.e2sont des objets différents avec des types différents. Cependant, z1.e1n'est pas imbriqué à l'intérieur z1.e2ni vice-versa. Et alors qu'ils ont des types différents, (Empty&)z1.e1et ne(Empty&)z1.e2 sont pas des types différents. Mais ils pointent vers des objets différents.

Et selon la règle d'identité unique, ils doivent avoir des adresses différentes. Ainsi , même si e1et e2sont théoriquement différents types, leurs internes doivent aussi Obey identité unique par rapport aux autres sous - objets dans le même objet contenant. Récursivement.

Ce que vous voulez est tout simplement impossible en C ++ tel qu'il est actuellement, quelle que soit la façon dont vous essayez.

Nicol Bolas
la source
Excellente explication, merci beaucoup!
tom
2

Pour autant que je sache, ce n'est pas possible si vous voulez avoir les deux membres. Mais vous pouvez vous spécialiser et n'avoir qu'un seul membre lorsque le type est identique et vide:

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

Bien sûr, le reste du programme qui utilise les membres devrait être modifié pour traiter le cas où il n'y a qu'un seul membre. Peu importe le membre utilisé dans ce cas - après tout, c'est un objet sans état sans adresse unique. Les fonctions membres affichées devraient simplifier les choses.

malheureusement sizeof(Empty<Empty<A,A>,A>{})==2où A est une structure complètement vide.

Vous pouvez introduire plus de spécialisations pour prendre en charge la compression récursive des paires vides:

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

Encore plus, pour compresser quelque chose comme 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
la source
C'est bien, mais malheureusement, sizeof(Empty<Empty<A,A>,A>{})==2où se Atrouve une structure complètement vide.
tom
J'ajouterais une get_empty<T>fonction. Ensuite, vous pouvez réutiliser le get_empty<T>à gauche ou à droite si cela fonctionne déjà là-bas.
Yakk - Adam Nevraumont