[[no_unique_address]] und zwei Mitgliedswerte desselben Typs

16

Ich spiele mit [[no_unique_address]]in herum c++20.

Im Beispiel für cppreference haben wir einen leeren Typ Emptyund TypZ

struct Empty {}; // empty class

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

Anscheinend muss die Größe von Zmindestens so sein, 2weil die Typen von e1und gleich e2sind.

Allerdings möchte ich wirklich Zmit Größe haben 1. Dies brachte mich zum Nachdenken, was ist mit dem Umschließen Emptyeiner Wrapper-Klasse mit zusätzlichen Vorlagenparametern, die verschiedene Arten von e1und erzwingen 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;
};

Leider sizeof(Z1)==2. Gibt es einen Trick, um die Größe Z1eins zu machen ?

Ich teste das mit gcc version 9.2.1undclang version 9.0.0


In meiner Bewerbung habe ich viele leere Typen des Formulars

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

Welches ist ein leerer Typ, wenn Tund Ssind auch leere Typen und verschieden! Ich mag diese Art leer sein , auch wenn Tund Ssind die gleichen Typen.

Tom
quelle
2
Was ist mit dem Hinzufügen von Vorlagenargumenten zu sich Tselbst? Das würde unterschiedliche Typen erzeugen. Im Moment hält Sie die Tatsache, dass beide Wrappererben T, zurück ...
Max Langhof
@MaxLanghof Was meinst du mit dem Hinzufügen eines Vorlagenarguments T? Im Moment Tist ein Vorlagenargument.
Tom
Nicht erben von T.
Evg
@ Evg macht hier keinen Unterschied.
Eerorika
2
Nur weil es größer ist als 1 macht es nicht nicht leer: coliru.stacked-crooked.com/a/51aa2be4aff4842e
Deduplicator

Antworten:

6

Welches ist ein leerer Typ, wenn Tund Ssind auch leere Typen und verschieden! Ich mag diese Art leer sein , auch wenn Tund Ssind die gleichen Typen.

Das kannst du nicht bekommen. Technisch gesehen kann man nicht einmal sicher , dass es auch dann , wenn leer ist Tund Ses verschiedene Typen leer. Denken Sie daran: no_unique_addressist ein Attribut; Die Fähigkeit, Objekte zu verbergen, ist vollständig Implementierung ab. Aus Sicht der Standards können Sie die Größe leerer Objekte nicht erzwingen.

Wenn C ++ 20-Implementierungen ausgereift sind, sollten Sie dies annehmen [[no_unique_address]] die Regeln für die Optimierung der leeren Basis im Allgemeinen werden. Solange zwei Objekte desselben Typs keine Unterobjekte sind, können Sie wahrscheinlich damit rechnen, dass sie sich verstecken. Aber an diesem Punkt ist es eine Art Pech.

Was den speziellen Fall Tund Sden gleichen Typ betrifft, ist dies einfach nicht möglich. Trotz der Auswirkungen des Namens "no_unique_address" erfordert C ++ in der Realität, dass bei zwei Zeigern auf Objekte desselben Typs diese Zeiger entweder auf dasselbe Objekt zeigen oder unterschiedliche Adressen haben. Ich nenne dies die "eindeutige Identitätsregel" und no_unique_addresshabe keinen Einfluss darauf. Aus [intro.object] / 9 :

Zwei Objekte mit überlappenden Lebensdauern, die keine Bitfelder sind, können dieselbe Adresse haben, wenn eines in das andere verschachtelt ist oder wenn mindestens eines ein Unterobjekt der Größe Null ist und sie unterschiedlichen Typs sind . Andernfalls haben sie unterschiedliche Adressen und belegen nicht zusammenhängende Speicherbytes.

Mitglieder von leeren Typen deklariert als [[no_unique_address]] Null sind, haben jedoch den gleichen Typ, was dies unmöglich macht.

Wenn Sie darüber nachdenken, verstößt der Versuch, den leeren Typ durch Verschachtelung auszublenden, immer noch gegen die eindeutige Identitätsregel. Betrachten Sie Ihren Wrapperund Z1Fall. Bei einer , z1die ist eine Instanz Z1, ist es klar , dass z1.e1und z1.e2verschiedene Objekte mit unterschiedlichen Typen sind. Ist z1.e1jedoch weder darin verschachtelt z1.e2noch umgekehrt. Und während sie haben verschiedene Arten, (Empty&)z1.e1und (Empty&)z1.e2sind nicht verschiedene Typen. Sie zeigen jedoch auf verschiedene Objekte.

Und nach der eindeutigen Identitätsregel müssen sie unterschiedliche Adressen haben. Also obwohl e1unde2 nominell unterschiedliche Typen sind, müssen ihre Interna auch einer eindeutigen Identität gegenüber anderen Unterobjekten in demselben enthaltenden Objekt gehorchen. Rekursiv.

Was Sie wollen, ist in C ++ in der jetzigen Form einfach unmöglich, unabhängig davon, wie Sie es versuchen.

Nicol Bolas
quelle
Tolle Erklärung, vielen Dank!
Tom
2

Soweit ich das beurteilen kann, ist dies nicht möglich, wenn Sie beide Mitglieder haben möchten. Sie können sich jedoch spezialisieren und nur eines der Mitglieder haben, wenn der Typ gleich und leer ist:

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

Natürlich müsste der Rest des Programms, das die Mitglieder verwendet, geändert werden, um den Fall zu behandeln, in dem es nur ein Mitglied gibt. Es sollte in diesem Fall keine Rolle spielen, welches Mitglied verwendet wird - schließlich handelt es sich um ein zustandsloses Objekt ohne eindeutige Adresse. Die gezeigten Elementfunktionen sollten dies vereinfachen.

leider sizeof(Empty<Empty<A,A>,A>{})==2wo A eine völlig leere Struktur ist.

Sie könnten weitere Spezialisierungen einführen, um die rekursive Komprimierung leerer Paare zu unterstützen:

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

Noch mehr, um so etwas zu komprimieren 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
quelle
Das ist schön, aber leider immer noch sizeof(Empty<Empty<A,A>,A>{})==2wo Aeine völlig leere Struktur ist.
Tom
Ich würde eine get_empty<T>Funktion hinzufügen . Dann können Sie das get_empty<T>links oder rechts wiederverwenden, wenn es dort bereits funktioniert.
Yakk - Adam Nevraumont