Saya telah menggunakan serikat pekerja sebelumnya dengan nyaman; hari ini saya terkejut ketika saya membaca posting ini dan mengetahui kode ini
union ARGB
{
uint32_t colour;
struct componentsTag
{
uint8_t b;
uint8_t g;
uint8_t r;
uint8_t a;
} components;
} pixel;
pixel.colour = 0xff040201; // ARGB::colour is the active member from now on
// somewhere down the line, without any edit to pixel
if(pixel.components.a) // accessing the non-active member ARGB::components
sebenarnya adalah perilaku yang tidak terdefinisi. Yaitu membaca dari anggota serikat selain yang baru-baru ini ditulis untuk mengarah pada perilaku yang tidak terdefinisi. Jika ini bukan tujuan penggunaan serikat, lalu apa? Bisakah seseorang menjelaskannya dengan terperinci?
Memperbarui:
Saya ingin mengklarifikasi beberapa hal di belakang.
- Jawaban untuk pertanyaan tidak sama untuk C dan C ++; diri saya yang lebih muda dan bodoh menandainya sebagai C dan C ++.
- Setelah menjelajahi melalui standar C ++ 11, saya tidak dapat secara meyakinkan mengatakan bahwa itu memanggil mengakses / memeriksa anggota serikat yang tidak aktif tidak ditentukan / tidak ditentukan / implementasi-didefinisikan. Yang bisa saya temukan adalah §9.5 / 1:
Jika penyatuan tata letak standar berisi beberapa struct tata letak standar yang berbagi urutan awal yang sama, dan jika objek tipe penyatuan tata letak standar ini berisi salah satu struktur tata letak standar, maka diizinkan untuk memeriksa urutan awal umum dari setiap anggota struct tata letak standar. §9.2 / 19: Dua struct tata letak standar berbagi urutan awal yang sama jika anggota yang sesuai memiliki tipe yang kompatibel dengan tata letak dan tidak ada anggota yang merupakan bidang bit atau keduanya adalah bidang bit dengan lebar yang sama untuk urutan satu atau lebih inisial anggota
- Sementara di C, ( C99 TC3 - DR 283 dan seterusnya) adalah sah untuk melakukannya ( terima kasih kepada Pascal Cuoq untuk membawa ini). Namun, upaya untuk melakukannya masih dapat menyebabkan perilaku tidak terdefinisi , jika nilai yang dibaca tidak valid (disebut "representasi perangkap") untuk jenis yang akan dibaca. Jika tidak, nilai yang dibaca adalah implementasi yang ditentukan.
C89 / 90 menyebutkan hal ini berdasarkan perilaku yang tidak ditentukan (Lampiran J) dan buku K&R mengatakan bahwa implementasinya didefinisikan. Kutipan dari K&R:
Ini adalah tujuan dari persatuan - variabel tunggal yang dapat secara sah menampung salah satu dari beberapa jenis. [...] selama penggunaannya konsisten: jenis yang diambil harus jenis yang paling baru disimpan. Merupakan tanggung jawab programmer untuk melacak jenis apa yang saat ini disimpan dalam serikat pekerja; hasilnya tergantung pada implementasi jika sesuatu disimpan sebagai satu jenis dan diekstraksi sebagai yang lain.
Ekstrak dari Stroustrup's TC ++ PL (tambang penekanan)
Penggunaan serikat pekerja dapat menjadi sangat penting untuk kesesuaian data [...] terkadang disalahgunakan untuk "konversi tipe ".
Di atas segalanya, pertanyaan ini (yang judulnya tetap tidak berubah sejak permintaan saya) diajukan dengan maksud untuk memahami tujuan serikat pekerja DAN tidak pada standar yang memungkinkan. Misalnya, menggunakan pewarisan untuk penggunaan kembali kode, tentu saja, diizinkan oleh standar C ++, tetapi itu bukan tujuan atau niat awal untuk memperkenalkan warisan sebagai fitur bahasa C ++ . Inilah alasan mengapa jawaban Andrey tetap seperti yang diterima.
sumber
b, g, r,
dana
mungkin tidak berdekatan, dan dengan demikian tidak cocok dengan tata letak auint32_t
. Ini merupakan tambahan untuk masalah Endianess yang telah ditunjukkan oleh orang lain.scouring C++11's standard I couldn't conclusively say that it calls out accessing/inspecting a non-active union member is undefined [...] All I could find was §9.5/1
...Betulkah? Anda mengutip catatan pengecualian , bukan poin utama tepat di awal paragraf : "Dalam sebuah serikat, paling banyak salah satu anggota data non-statis dapat aktif kapan saja, yaitu nilai paling banyak dari satu anggota data non-statis dapat disimpan dalam serikat kapan saja. " - dan turun ke p4: "Secara umum, seseorang harus menggunakan panggilan destruktor eksplisit dan penempatan operator baru untuk mengubah anggota aktif serikat "Jawaban:
Tujuan serikat agak jelas, tetapi untuk beberapa alasan orang sering melewatkannya.
Tujuan penyatuan adalah untuk menghemat memori dengan menggunakan wilayah memori yang sama untuk menyimpan objek yang berbeda pada waktu yang berbeda. Itu dia.
Itu seperti sebuah kamar di sebuah hotel. Orang yang berbeda hidup di dalamnya selama periode waktu yang tidak tumpang tindih. Orang-orang ini tidak pernah bertemu, dan umumnya tidak tahu apa-apa tentang satu sama lain. Dengan mengelola pembagian waktu kamar dengan benar (yaitu dengan memastikan orang yang berbeda tidak ditugaskan ke satu kamar pada saat yang sama), sebuah hotel yang relatif kecil dapat menyediakan akomodasi untuk sejumlah besar orang, yang merupakan hotel adalah untuk.
Persis seperti itulah yang dilakukan serikat pekerja. Jika Anda tahu bahwa beberapa objek dalam program Anda memiliki nilai dengan nilai-nilai yang tidak tumpang tindih, maka Anda dapat "menggabungkan" objek-objek ini menjadi satu kesatuan dan dengan demikian menghemat memori. Sama seperti kamar hotel memiliki paling banyak satu penyewa "aktif" pada setiap saat, serikat memiliki paling banyak satu anggota "aktif" pada setiap saat waktu program. Hanya anggota "aktif" yang dapat dibaca. Dengan menulis ke anggota lain Anda mengubah status "aktif" ke anggota lain itu.
Untuk beberapa alasan, tujuan awal serikat pekerja ini "ditimpa" dengan sesuatu yang sama sekali berbeda: menulis satu anggota serikat pekerja dan kemudian memeriksanya melalui anggota lain. Reinterpretasi memori jenis ini (alias "tipe punning")
bukan penggunaan serikat yang valid. Secara umum hal ini mengarah pada perilaku tidak terdefinisiyang digambarkan sebagai menghasilkan perilaku implementasi-didefinisikan dalam C89 / 90.EDIT: Menggunakan serikat untuk tujuan penghukuman jenis (yaitu menulis satu anggota dan kemudian membaca yang lain) diberi definisi yang lebih rinci dalam salah satu Corrigenda Teknis dengan standar C99 (lihat DR # 257 dan DR # 283 ). Namun, perlu diingat bahwa ini secara formal tidak melindungi Anda dari berlari ke perilaku yang tidak terdefinisi dengan mencoba membaca representasi perangkap.
sumber
<time.h>
pada Windows dan Unix. Mengabaikannya sebagai "tidak valid" dan "tidak terdefinisi" tidak cukup memadai jika saya akan dipanggil untuk memahami kode yang bekerja dengan cara yang tepat seperti ini.Anda bisa menggunakan serikat pekerja untuk membuat struct seperti berikut ini, yang berisi bidang yang memberitahu kami komponen serikat mana yang sebenarnya digunakan:
sumber
int
atauchar*
untuk 10 item objek []; dalam hal ini, saya benar-benar dapat mendeklarasikan struct terpisah untuk setiap tipe data, bukan VAROBJECT? Bukankah itu mengurangi kekacauan dan menggunakan ruang yang lebih kecil?Perilaku tidak terdefinisi dari sudut pandang bahasa. Pertimbangkan bahwa platform yang berbeda dapat memiliki batasan yang berbeda dalam penyelarasan memori dan endianness. Kode dalam big endian versus mesin endian kecil akan memperbarui nilai dalam struct secara berbeda. Memperbaiki perilaku dalam bahasa akan membutuhkan semua implementasi untuk menggunakan endianness yang sama (dan kendala penyelarasan memori ...) membatasi penggunaan.
Jika Anda menggunakan C ++ (Anda menggunakan dua tag) dan Anda benar-benar peduli tentang portabilitas, maka Anda bisa menggunakan struct dan menyediakan setter yang mengambil
uint32_t
dan mengatur bidang dengan tepat melalui operasi bitmask. Hal yang sama dapat dilakukan dalam C dengan suatu fungsi.Sunting : Saya mengharapkan Pemrogram untuk menuliskan jawaban untuk memilih dan menutup yang ini. Seperti yang ditunjukkan beberapa komentar, endianness dibahas di bagian lain standar dengan membiarkan setiap implementasi memutuskan apa yang harus dilakukan, dan pelurusan dan bantalan juga dapat ditangani secara berbeda. Sekarang, aturan aliasing yang ketat yang mengacu oleh AProgrammer adalah poin penting di sini. Kompiler diperbolehkan untuk membuat asumsi tentang modifikasi (atau kurangnya modifikasi) variabel. Dalam kasus gabungan, kompiler dapat menyusun ulang instruksi dan memindahkan pembacaan masing-masing komponen warna di atas tulisan ke variabel warna.
sumber
Yang paling umum penggunaan
union
saya secara teratur menemukan yang aliasing .Pertimbangkan yang berikut ini:
Apa fungsinya? Hal ini memungkinkan bersih, akses rapi dari
Vector3f vec;
anggota 's dengan baik nama:atau dengan akses integer ke dalam array
Dalam beberapa kasus, mengakses dengan nama adalah hal paling jelas yang dapat Anda lakukan. Dalam kasus lain, terutama ketika sumbu dipilih secara terprogram, hal yang lebih mudah dilakukan adalah mengakses sumbu dengan indeks numerik - 0 untuk x, 1 untuk y, dan 2 untuk z.
sumber
type-punning
yang juga disebutkan dalam pertanyaan. Contoh dalam pertanyaan juga menunjukkan contoh yang serupa.Seperti yang Anda katakan, ini adalah perilaku yang sangat tidak terdefinisi, meskipun itu akan "bekerja" pada banyak platform. Alasan sebenarnya untuk menggunakan serikat adalah untuk membuat catatan varian.
Tentu saja, Anda juga perlu semacam diskriminator untuk mengatakan apa varian sebenarnya mengandung. Dan perhatikan bahwa dalam C ++ serikat tidak banyak digunakan karena mereka hanya dapat berisi tipe POD - secara efektif yang tanpa konstruktor dan destruktor.
sumber
Di C itu adalah cara yang bagus untuk mengimplementasikan sesuatu seperti varian.
Pada saat memori litlle struktur ini menggunakan memori kurang dari struct yang memiliki semua anggota.
By the way C menyediakan
untuk mengakses nilai bit.
sumber
Meskipun ini adalah perilaku yang sangat tidak terdefinisi, dalam praktiknya ia akan bekerja dengan hampir semua kompiler. Ini adalah paradigma yang banyak digunakan sehingga setiap kompiler yang menghargai diri sendiri perlu melakukan "hal yang benar" dalam kasus-kasus seperti ini. Tentunya lebih disukai daripada tipe-punning, yang mungkin menghasilkan kode rusak dengan beberapa kompiler.
sumber
Dalam C ++, Boost Variant mengimplementasikan versi serikat yang aman, yang dirancang untuk mencegah perilaku tidak terdefinisi sebanyak mungkin.
Penampilannya identik dengan
enum + union
konstruk (stack dialokasikan terlalu dll) tetapi menggunakan daftar templat jenis bukanenum
:)sumber
Perilaku tersebut mungkin tidak terdefinisi, tetapi itu berarti tidak ada "standar". Semua kompiler yang layak menawarkan #pragma untuk mengontrol pengemasan dan perataan, tetapi mungkin memiliki standar yang berbeda. Standarnya juga akan berubah tergantung pada pengaturan optimasi yang digunakan.
Juga, serikat pekerja tidak hanya untuk menghemat ruang. Mereka dapat membantu kompiler modern dengan jenis hukuman. Jika Anda
reinterpret_cast<>
semuanya kompiler tidak dapat membuat asumsi tentang apa yang Anda lakukan. Mungkin harus membuang apa yang diketahui tentang jenis Anda dan mulai lagi (memaksa menulis kembali ke memori, yang sangat tidak efisien hari ini dibandingkan dengan kecepatan jam CPU).sumber
Secara teknis itu tidak terdefinisi, tetapi dalam kenyataannya sebagian besar (semua?) Kompiler memperlakukannya persis sama dengan menggunakan
reinterpret_cast
dari satu jenis ke yang lain, yang hasilnya didefinisikan implementasi. Saya tidak akan kehilangan tidur karena kode Anda saat ini.sumber
Untuk satu contoh lagi tentang penggunaan aktual serikat pekerja, kerangka kerja CORBA membuat serialisasi objek menggunakan pendekatan serikat yang ditandai. Semua kelas yang ditentukan pengguna adalah anggota satu serikat (besar), dan pengenal bilangan bulat memberi tahu demarshaller cara menafsirkan serikat.
sumber
Yang lain menyebutkan perbedaan arsitektur (little - big endian).
Saya membaca masalah bahwa karena memori untuk variabel dibagi, kemudian dengan menulis ke satu, yang lain berubah dan, tergantung pada jenisnya, nilainya bisa menjadi tidak berarti.
misalnya. union {float f; int i; } x;
Menulis ke xi akan menjadi tidak berarti jika Anda kemudian membaca dari xf - kecuali jika itu yang Anda maksud untuk melihat tanda, komponen eksponen atau mantissa dari float.
Saya pikir ada juga masalah perataan: Jika beberapa variabel harus selaras kata maka Anda mungkin tidak mendapatkan hasil yang diharapkan.
misalnya. union {char c [4]; int i; } x;
Jika, secara hipotetis, pada beberapa mesin char harus sejajar kata maka c [0] dan c [1] akan berbagi penyimpanan dengan i tetapi tidak c [2] dan c [3].
sumber
memcpy()
dari satu ke yang lain. Beberapa sistem mungkin secara spekulatif menyelaraskanchar[]
alokasi yang terjadi di luar struktur / serikat pekerja untuk alasan itu dan lainnya. Dalam contoh yang ada, asumsi yangi
akan tumpang tindih semua untuk elemenc[]
adalah non-portabel, tapi itu karena tidak ada jaminan itusizeof(int)==4
.Dalam bahasa C seperti yang didokumentasikan pada tahun 1974, semua anggota struktur berbagi ruang nama yang sama, dan arti "ptr-> anggota" didefinisikan sebagai menambahkan perpindahan anggota ke "ptr" dan mengakses alamat yang dihasilkan menggunakan jenis anggota. Desain ini memungkinkan untuk menggunakan ptr yang sama dengan nama anggota yang diambil dari definisi struktur yang berbeda tetapi dengan offset yang sama; programmer menggunakan kemampuan itu untuk berbagai keperluan.
Ketika anggota struktur diberi ruang nama mereka sendiri, menjadi tidak mungkin untuk mendeklarasikan dua anggota struktur dengan perpindahan yang sama. Menambahkan serikat pekerja ke bahasa memungkinkan untuk mencapai semantik yang sama yang telah tersedia di versi bahasa sebelumnya (meskipun ketidakmampuan untuk memiliki nama diekspor ke konteks terlampir mungkin masih perlu menggunakan find / replace untuk mengganti foo-> anggota ke foo-> type1.member). Yang penting bukanlah bahwa orang-orang yang menambahkan serikat memiliki target penggunaan tertentu dalam pikiran, tetapi bahwa mereka menyediakan sarana dimana programmer yang mengandalkan semantik sebelumnya, untuk tujuan apa pun , masih harus dapat mencapai semantik yang sama bahkan jika mereka harus menggunakan sintaks yang berbeda untuk melakukannya.
sumber
Anda dapat menggunakan serikat pekerja karena dua alasan utama:
1 Benar-benar lebih dari peretasan gaya-C untuk memotong kode penulisan dengan dasar Anda tahu bagaimana arsitektur memori sistem target bekerja. Seperti yang sudah dikatakan, Anda biasanya bisa lolos jika Anda tidak benar-benar menargetkan banyak platform yang berbeda. Saya percaya beberapa penyusun mungkin membiarkan Anda menggunakan arahan pengepakan juga (saya tahu mereka lakukan pada struct)?
Contoh 2. yang baik dapat ditemukan dalam tipe VARIAN yang digunakan secara luas dalam COM.
sumber
Seperti yang disebutkan lainnya, serikat pekerja yang dikombinasikan dengan enumerasi dan dibungkus dengan struct dapat digunakan untuk mengimplementasikan serikat yang ditandai. Salah satu penggunaan praktis adalah untuk mengimplementasikan Rust
Result<T, E>
, yang awalnya diimplementasikan menggunakan murnienum
(Rust dapat menyimpan data tambahan dalam varian enumerasi). Berikut ini adalah contoh C ++:sumber