[[no_unique_address]] dan dua nilai anggota dengan tipe yang sama

16

Aku bermain-main dengan [[no_unique_address]]di c++20.

Dalam contoh pada cppreference kita memiliki tipe Emptydan tipe kosongZ

struct Empty {}; // empty class

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

Rupanya, ukuran Zharus setidaknya 2karena jenis e1dan e2sama.

Namun, saya benar-benar ingin memiliki Zukuran 1. Ini membuat saya berpikir, bagaimana dengan membungkus Emptybeberapa kelas pembungkus dengan parameter templat tambahan yang memberlakukan berbagai jenis e1dan 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;
};

Sayangnya sizeof(Z1)==2,. Apakah ada trik untuk membuat ukuran Z1menjadi satu?

Saya menguji ini dengan gcc version 9.2.1danclang version 9.0.0


Dalam aplikasi saya, saya punya banyak jenis formulir kosong

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

Yang merupakan tipe kosong jika Tdan Sjuga tipe kosong dan berbeda! Saya ingin jenis ini kosong bahkan jika Tdan Smerupakan tipe yang sama.

tom
sumber
2
Bagaimana dengan menambahkan argumen templat ke Tdirinya sendiri? Itu akan menghasilkan tipe yang berbeda. Sekarang fakta bahwa kedua Wrapperwarisan Titu
menahanmu
@ MaxLanghof Apa maksudmu dengan menambahkan argumen template T? Sekarang, Tadalah argumen templat.
tom
Jangan mewarisi dari T.
Evg
@ Evg tidak ada bedanya di sini.
eerorika
2
Hanya karena lebih besar dari 1 tidak membuatnya tidak kosong: coliru.stacked-crooked.com/a/51aa2be4aff4842e
Deduplicator

Jawaban:

6

Yang merupakan tipe kosong jika Tdan Sjuga tipe kosong dan berbeda! Saya ingin jenis ini kosong bahkan jika Tdan Smerupakan tipe yang sama.

Anda tidak bisa mendapatkannya. Secara teknis, Anda bahkan tidak dapat menjamin bahwa itu akan kosong walaupun Tdan Smerupakan tipe kosong yang berbeda. Ingat: no_unique_addressadalah atribut; kemampuannya untuk menyembunyikan objek sepenuhnya tergantung pada implementasi. Dari perspektif standar, Anda tidak bisa memaksakan ukuran objek kosong.

Saat implementasi C ++ 20 matang, Anda harus menganggap bahwa [[no_unique_address]]umumnya akan mengikuti aturan optimasi basis kosong. Yaitu, selama dua objek dari jenis yang sama bukan sub-objek, Anda mungkin bisa berharap untuk bersembunyi. Tetapi pada titik ini, itu semacam keberuntungan.

Mengenai kasus spesifik Tdan Sjenis yang sama, itu sama sekali tidak mungkin. Terlepas dari implikasi nama "no_unique_address", kenyataannya adalah bahwa C ++ mengharuskan, mengingat dua pointer ke objek dari tipe yang sama, pointer tersebut menunjuk ke objek yang sama atau memiliki alamat yang berbeda. Saya menyebutnya "aturan identitas unik", dan no_unique_addresstidak memengaruhi hal itu. Dari [intro.object] / 9 :

Dua objek dengan masa hidup yang tumpang tindih yang bukan bidang bit dapat memiliki alamat yang sama jika satu bersarang di dalam yang lain, atau jika setidaknya satu adalah sub-objek ukuran nol dan mereka dari jenis yang berbeda ; jika tidak, mereka memiliki alamat yang berbeda dan menempati byte penyimpanan yang terpisah.

Anggota tipe kosong dinyatakan sebagai [[no_unique_address]] berukuran nol, tetapi memiliki tipe yang sama membuat ini tidak mungkin.

Memang, memikirkannya, berusaha menyembunyikan tipe kosong melalui sarang masih melanggar aturan identitas yang unik. Pertimbangkan Anda Wrapperdan Z1kasus. Diberikan z1yang merupakan contoh dari Z1, jelas bahwa z1.e1dan z1.e2adalah objek yang berbeda dengan tipe yang berbeda. Namun, z1.e1tidak bersarang di dalam z1.e2atau sebaliknya. Dan sementara mereka memiliki jenis yang berbeda, (Empty&)z1.e1dan (Empty&)z1.e2yang tidak jenis yang berbeda. Tetapi mereka menunjuk ke objek yang berbeda.

Dan dengan aturan identitas unik, mereka harus memiliki alamat yang berbeda. Jadi meskipun e1dan e2secara nominal berbeda jenis, bagian dalamnya juga harus mematuhi identitas unik terhadap sub-objek lain dalam objek yang berisi yang sama. Secara rekursif.

Apa yang Anda inginkan tidak mungkin dilakukan dalam C ++ seperti saat ini, terlepas dari bagaimana Anda mencoba.

Nicol Bolas
sumber
Penjelasan yang bagus, terima kasih banyak!
tom
2

Sejauh yang saya tahu, itu tidak mungkin jika Anda ingin memiliki kedua anggota. Tetapi Anda dapat mengkhususkan dan hanya memiliki satu anggota ketika jenisnya sama dan kosong:

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

Tentu saja, sisa program yang menggunakan anggota perlu diubah untuk menangani kasus di mana hanya ada satu anggota. Seharusnya tidak masalah anggota mana yang digunakan dalam kasus ini - lagipula, itu adalah objek stateless tanpa alamat unik. Fungsi anggota yang ditampilkan harus sesederhana itu.

sayangnya di sizeof(Empty<Empty<A,A>,A>{})==2mana A adalah struct yang benar-benar kosong.

Anda dapat memperkenalkan lebih banyak spesialisasi untuk mendukung kompresi rekursif pasangan kosong:

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

Terlebih lagi, untuk mengompresi sesuatu seperti 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
sumber
Ini bagus, tapi masih sayang sizeof(Empty<Empty<A,A>,A>{})==2mana Aadalah struct benar-benar kosong.
tom
Saya akan menambahkan get_empty<T>fungsi. Kemudian Anda dapat menggunakan kembali get_empty<T>di kiri atau kanan jika sudah berfungsi di sana.
Yakk - Adam Nevraumont