web-dev-qa-db-fra.com

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

Je joue avec [[no_unique_address]] Dans c++20.

Dans l'exemple sur cppreference nous avons un type vide Empty et type Z

struct Empty {}; // empty class

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

Apparemment, la taille de Z doit être d'au moins 2 Car les types de e1 Et e2 Sont les mêmes.

Cependant, je veux vraiment avoir Z avec la taille 1. Cela m'a fait réfléchir, qu'en est-il de l'emballage Empty dans une classe wrapper avec un paramètre de modèle supplémentaire qui applique différents types de e1 Et 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. Existe-t-il une astuce pour faire de la taille de Z1 Une unité?

Je teste ceci avec gcc version 9.2.1 Et clang 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;
};

Quel est un type vide si T et S sont également des types vides et distincts! Je veux que ce type soit vide même si T et S sont les mêmes types.

16
tom

Quel est un type vide si T et S sont également des types vides et distincts! Je veux que ce type soit vide même si T et S sont 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 T et S sont des types vides différents. Rappelles toi: no_unique_address est un attribut; sa capacité à cacher des objets dépend de - 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 que [[no_unique_address]] suivra 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.

Quant au cas spécifique où T et S sont du 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_address n'affecte pas cela. De [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.

Membres de types vides déclarés comme [[no_unique_address]] sont de taille nulle, mais avoir le même type rend 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 Wrapper et Z1 Cas. Donné un z1 qui est une instance de Z1, il est clair que z1.e1 et z1.e2 sont différents objets avec différents types. Cependant, z1.e1 n'est pas imbriqué dans z1.e2 ni vice-versa. Et bien qu'ils aient différents types, (Empty&)z1.e1 et (Empty&)z1.e2 sont pas différents types. Mais ils pointent vers des objets différents.

Et selon la règle d'identité unique, ils doivent ont des adresses différentes. Donc même si e1 et e2 sont des types nominalement différents, leurs éléments internes doivent également obéir à une identité unique par rapport à d'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.

6
Nicol Bolas

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>{})==2 où 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; };
};
2
eerorika