[[no_unique_address]] i dwie wartości składowe tego samego typu

16

Mam zabawy z [[no_unique_address]]w c++20.

W przykładzie na cppreference mamy pusty typ Emptyi typZ

struct Empty {}; // empty class

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

Najwyraźniej rozmiar Zmusi być co najmniej 2dlatego, że typy e1i e2są takie same.

Jednak naprawdę chcę mieć Zrozmiar 1. To dało mi do myślenia, co z zawijaniem Emptyw jakiejś klasie opakowania z dodatkowym parametrem szablonu, który wymusza różne typy e1i 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;
};

Niestety sizeof(Z1)==2. Czy jest jakiś sposób, aby zrobić z niego rozmiar Z1?

Testuję to za pomocą gcc version 9.2.1iclang version 9.0.0


W mojej aplikacji mam wiele pustych typów formularza

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

Który jest typem pustym, jeśli Ti Ssą również typami pustymi i odrębnymi! Chcę tego typu mają być puste, nawet jeśli Ti Sto te same typy.

Tomek
źródło
2
Co z dodawaniem argumentów szablonu do Tsiebie? To generowałoby różne typy. W tej chwili fakt, że obaj Wrapperdziedziczymy, Tpowstrzymuje cię ...
Max Langhof,
@MaxLanghof Co rozumiesz przez dodanie argumentu szablonu T? W tej chwili Tjest argumentem szablonu.
tom
Nie dziedzicz po T.
Evg,
@Evg nie ma tutaj znaczenia.
eerorika
2
Tylko dlatego, że jest większy niż 1, nie czyni go niepustym: coliru.stacked-crooked.com/a/51aa2be4aff4842e
Deduplicator

Odpowiedzi:

6

Który jest typem pustym, jeśli Ti Ssą również typami pustymi i odrębnymi! Chcę tego typu mają być puste, nawet jeśli Ti Sto te same typy.

Nie możesz tego zdobyć. Technicznie rzecz biorąc, nie można nawet zagwarantować, że będzie on pusty, nawet jeśli Ti Ssą różne rodzaje pusty. Pamiętaj: no_unique_addressjest atrybutem; zdolność do ukrywania obiektów jest całkowicie zależna od implementacji. Ze standardowego punktu widzenia nie można narzucać wielkości pustych obiektów.

W miarę dojrzewania implementacji C ++ 20 należy założyć, że [[no_unique_address]]będą one generalnie przestrzegać zasad optymalizacji pustej bazy. Mianowicie, dopóki dwa obiekty tego samego typu nie są podobiektami, prawdopodobnie można się spodziewać ukrycia. Ale w tym momencie to trochę szczęścia.

Jeśli chodzi o konkretny przypadek Ti Sbycie tego samego typu, jest to po prostu niemożliwe. Pomimo implikacji nazwy „no_unique_address”, w rzeczywistości C ++ wymaga, aby biorąc pod uwagę dwa wskaźniki dla obiektów tego samego typu, wskaźniki te wskazywały na ten sam obiekt lub miały różne adresy. Nazywam to „unikalną zasadą tożsamości” i no_unique_addressnie ma na to wpływu. Z [intro.object] / 9 :

Dwa obiekty z nakładającymi się okresami istnienia, które nie są polami bitowymi, mogą mieć ten sam adres, jeśli jeden jest zagnieżdżony w drugim lub jeśli co najmniej jeden jest podobiektem o zerowej wielkości i są one różnego rodzaju ; w przeciwnym razie mają odrębne adresy i zajmują rozłączne bajty pamięci.

Członkowie pustych typów zadeklarowanych jako [[no_unique_address]]mają rozmiar zero, ale posiadanie tego samego typu uniemożliwia to.

Rzeczywiście, myślenie o tym, próba ukrycia pustego typu poprzez zagnieżdżanie nadal narusza unikalną zasadę tożsamości. Rozważ swoją Wrapperi swoją Z1sprawę. Biorąc pod uwagę z1, który jest instancją Z1, oczywiste jest, że z1.e1i z1.e2różne przedmioty z różnych typów. Nie z1.e1jest jednak zagnieżdżony z1.e2ani odwrotnie. I chociaż mają różne typy (Empty&)z1.e1i nie(Empty&)z1.e2 są różnymi typami. Ale wskazują na różne przedmioty.

Zgodnie z unikalną regułą tożsamości muszą mieć różne adresy. Więc chociaż e1i e2są nominalnie różne typy, ich wewnętrzne muszą również przestrzegać następujących zaleceń unikalna tożsamość wobec innych podobiektów w tym samym obiekcie zawierającym. Rekurencyjnie.

To, czego chcesz, jest po prostu niemożliwe w C ++ w obecnej postaci, niezależnie od tego, jak spróbujesz.

Nicol Bolas
źródło
Świetne wyjaśnienie, wielkie dzięki!
tom
2

O ile mi wiadomo, nie jest to możliwe, jeśli chcesz mieć obu członków. Ale możesz się specjalizować i mieć tylko jednego członka, gdy typ jest taki sam i pusty:

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

Oczywiście reszta programu, która korzysta z członków, musiałaby zostać zmieniona, aby zająć się przypadkiem, w którym jest tylko jeden członek. Nie powinno mieć znaczenia, który element zostanie użyty w tym przypadku - w końcu jest to obiekt bezstanowy bez unikalnego adresu. Pokazane funkcje składowe powinny to ułatwić.

niestety, sizeof(Empty<Empty<A,A>,A>{})==2gdzie A jest całkowicie pustą strukturą.

Możesz wprowadzić więcej specjalizacji w celu obsługi rekurencyjnej kompresji pustych par:

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

Co więcej, aby kompresować coś takiego 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
źródło
To miłe, ale wciąż niestety sizeof(Empty<Empty<A,A>,A>{})==2gdzie Ajest całkowicie pusta struktura.
tom
Dodałbym get_empty<T>funkcję. Następnie możesz ponownie użyć get_empty<T>lewej lub prawej, jeśli już tam działa.
Jak - Adam Nevraumont