Mengakses anggota serikat yang tidak aktif dan perilaku yang tidak terdefinisi?

129

Saya mendapat kesan bahwa mengakses unionanggota selain dari set terakhir adalah UB, tetapi saya sepertinya tidak dapat menemukan referensi yang kuat (selain jawaban yang mengklaim itu adalah UB tetapi tanpa dukungan dari standar).

Jadi, apakah itu perilaku yang tidak terdefinisi?

Luchian Grigore
sumber
3
C99 (dan saya percaya C ++ 11 juga) secara eksplisit memungkinkan jenis-hukuman dengan serikat pekerja. Jadi saya pikir itu termasuk dalam perilaku "implementasi yang ditentukan".
Mysticial
1
Saya telah menggunakannya pada beberapa kesempatan untuk mengkonversi dari int individu ke char. Jadi, saya pasti tahu itu tidak terdefinisi. Saya menggunakannya pada kompiler Sun CC. Jadi, itu mungkin masih bergantung pada kompiler.
go4sri
42
@ go4sri: Jelas, Anda tidak tahu apa artinya perilaku tidak terdefinisi. Kenyataan bahwa itu tampaknya bekerja untuk Anda dalam beberapa kasus tidak bertentangan dengan tidak terdefinisi.
Benjamin Lindley
4
@Mysticial, posting blog yang Anda tautkan sangat spesifik mengenai C99; pertanyaan ini hanya ditandai untuk C ++.
davmac

Jawaban:

131

Kebingungannya adalah bahwa C secara eksplisit memungkinkan jenis-menghukum melalui serikat pekerja, sedangkan C ++ () tidak memiliki izin seperti itu.

6.5.2.3 Struktur dan anggota serikat

95) Jika anggota yang digunakan untuk membaca konten objek penyatuan tidak sama dengan anggota yang terakhir digunakan untuk menyimpan nilai dalam objek, bagian yang sesuai dari representasi objek dari nilai ditafsirkan kembali sebagai representasi objek di objek baru. ketik seperti yang dijelaskan dalam 6.2.6 (proses yang kadang-kadang disebut '' type punning ''). Ini mungkin representasi jebakan.

Situasi dengan C ++:

9.5 Serikat [class.union]

Dalam serikat pekerja, 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.

C ++ nantinya memiliki bahasa yang mengizinkan penggunaan serikat yang berisi struct s dengan urutan awal umum; Namun ini tidak mengizinkan hukuman jenis.

Untuk menentukan apakah jenis serikat hukuman yang diperbolehkan dalam C ++, kita harus mencari lebih lanjut. Ingat itu adalah referensi normatif untuk C ++ 11 (dan C99 memiliki bahasa yang mirip dengan C11 yang memungkinkan penghukuman jenis serikat):

3.9 Jenis [basic.types]

4 - Representasi objek dari objek tipe T adalah urutan objek char unsigned N yang diambil oleh objek tipe T, di mana N sama dengan sizeof (T). Representasi nilai dari suatu objek adalah sekumpulan bit yang menyimpan nilai tipe T. Untuk tipe yang dapat disalin secara trivial, representasi nilai adalah sekumpulan bit dalam representasi objek yang menentukan nilai, yang merupakan salah satu elemen diskrit dari implementasi- set nilai yang ditentukan. 42
42) Maksudnya adalah bahwa model memori C ++ kompatibel dengan yang dari Bahasa Pemrograman ISO / IEC 9899 C.

Ini menjadi sangat menarik ketika kita membaca

3.8 Obyek seumur hidup [basic.life]

Umur objek tipe T dimulai ketika: - penyimpanan dengan perataan dan ukuran yang tepat untuk tipe T diperoleh, dan - jika objek memiliki inisialisasi non-sepele, inisialisasi lengkap.

Jadi untuk tipe primitif (yang ipso facto memiliki inisialisasi sepele) yang terkandung dalam persatuan, umur objek mencakup setidaknya masa perserikatan itu sendiri. Ini memungkinkan kita untuk memohon

3.9.2 Jenis senyawa [basic.compound]

Jika objek tipe T terletak di alamat A, pointer tipe cv T * yang nilainya adalah alamat A dikatakan menunjuk ke objek itu, terlepas dari bagaimana nilai itu diperoleh.

Dengan asumsi bahwa operasi yang kami minati adalah tipe-punning yaitu mengambil nilai anggota serikat yang tidak aktif, dan diberikan sesuai dengan di atas bahwa kami memiliki referensi yang valid ke objek yang dirujuk oleh anggota tersebut, operasi itu bernilai rendah untuk -nilai konversi:

4.1 Konversi nilai-ke-nilai [conv.lval]

Glvalue tipe non-fungsi, non-array Tdapat dikonversi ke nilai awal. Jika Tmerupakan tipe yang tidak lengkap, program yang memerlukan konversi ini tidak terbentuk dengan baik. Jika objek yang dirujuk oleh glvalue bukanlah objek bertipe Tdan bukan objek bertipe berasal T, atau jika objek tidak diinisialisasi, program yang mengharuskan konversi ini memiliki perilaku yang tidak ditentukan.

Pertanyaannya kemudian adalah apakah suatu objek yang merupakan anggota serikat tidak aktif diinisialisasi dengan penyimpanan ke anggota serikat aktif. Sejauh yang saya tahu, ini tidak terjadi dan meskipun jika:

  • serikat disalin ke dalam charpenyimpanan array dan kembali (3.9: 2), atau
  • sebuah serikat secara bersamaan disalin ke serikat lain dengan tipe yang sama (3.9: 3), atau
  • sebuah serikat diakses melintasi batas-batas bahasa oleh elemen program yang sesuai dengan ISO / IEC 9899 (sejauh yang didefinisikan) (3.9: 4 catatan 42), kemudian

akses ke persatuan oleh anggota tidak aktif didefinisikan dan didefinisikan untuk mengikuti representasi objek dan nilai, akses tanpa salah satu dari interposisi di atas adalah perilaku yang tidak terdefinisi. Ini memiliki implikasi untuk optimisasi yang diperbolehkan untuk dilakukan pada program seperti itu, karena implementasi tentu saja dapat mengasumsikan bahwa perilaku yang tidak terdefinisi tidak terjadi.

Yaitu, meskipun kita dapat secara sah membentuk nilai untuk anggota serikat yang tidak aktif (itulah sebabnya menugaskan anggota yang tidak aktif tanpa konstruksi boleh saja) itu dianggap tidak diinisialisasi.

ecatmur
sumber
5
3.8 / 1 mengatakan masa hidup suatu objek berakhir ketika penyimpanannya digunakan kembali. Itu menunjukkan kepada saya bahwa anggota tidak aktif seumur hidup serikat telah berakhir karena penyimpanannya telah digunakan kembali untuk anggota aktif. Itu berarti Anda terbatas dalam cara Anda menggunakan anggota (3.8 / 6).
bames53
2
Di bawah interpretasi itu, maka setiap bit memori secara bersamaan berisi objek dari semua jenis yang dapat diinisiasi secara sepele dan memiliki penyelarasan yang sesuai ... Jadi, apakah masa pakai semua jenis yang dapat diprakarsai secara non-trivial segera berakhir karena penyimpanannya digunakan kembali untuk semua jenis lainnya ( dan tidak memulai kembali karena mereka tidak dapat diprakarsai secara sepele)?
bames53
3
Kata-kata 4.1 benar-benar dan benar-benar rusak dan sejak itu ditulis ulang. Itu melarang segala macam hal yang benar-benar valid: ia melarang memcpyimplementasi kustom (mengakses objek menggunakan nilai- unsigned charnilai), ia melarang akses ke *psetelah int *p = 0; const int *const *pp = &p;(meskipun konversi implisit dari int**ke const int*const*valid), ia melarang bahkan mengakses csetelah struct S s; const S &c = s;. Masalah CWG 616 . Apakah kata-kata baru itu memungkinkan? Ada juga [basic.lval].
2
@Omnifarious: Itu masuk akal, meskipun itu juga perlu mengklarifikasi (dan Standar C juga perlu mengklarifikasi, btw) apa arti &operator unary ketika diterapkan pada anggota serikat. Saya akan berpikir pointer yang dihasilkan harus dapat digunakan untuk mengakses anggota setidaknya sampai waktu berikutnya langsung atau tidak langsung menggunakan nilai anggota lain, tetapi dalam gcc pointer tidak dapat digunakan bahkan selama itu, yang menimbulkan pertanyaan tentang apa yang &operator seharusnya berarti.
supercat
4
Satu pertanyaan mengenai "Ingat bahwa c99 adalah referensi normatif untuk C ++ 11" Bukankah itu hanya relevan, di mana standar c ++ secara eksplisit mengacu pada standar C (misalnya untuk fungsi pustaka c)?
MikeMB
28

Standar C ++ 11 mengatakan seperti ini

9.5 Serikat Pekerja

Dalam serikat pekerja, 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.

Jika hanya satu nilai yang disimpan, bagaimana Anda bisa membaca yang lain? Itu tidak ada.


Dokumentasi gcc mencantumkan ini di bawah Perilaku yang ditentukan implementasi

  • Anggota objek gabungan diakses menggunakan anggota dengan tipe berbeda (C90 6.3.2.3).

Bytes yang relevan dari representasi objek diperlakukan sebagai objek dari tipe yang digunakan untuk akses. Lihat Jenis-hukuman. Ini mungkin representasi jebakan.

menunjukkan bahwa ini tidak diperlukan oleh standar C.


2016-01-05: Melalui komentar saya ditautkan dengan C99 Defect Report # 283 yang menambahkan teks yang mirip dengan catatan kaki ke dokumen standar C:

78a) Jika anggota yang digunakan untuk mengakses konten objek penyatuan tidak sama dengan anggota yang terakhir digunakan untuk menyimpan nilai dalam objek, bagian yang sesuai dari representasi objek dari nilai ditafsirkan kembali sebagai representasi objek di objek baru. ketik seperti yang dijelaskan dalam 6.2.6 (proses yang kadang-kadang disebut "ketik punning"). Ini mungkin representasi jebakan.

Tidak yakin apakah itu menjelaskan banyak, mengingat catatan kaki tidak normatif untuk standar.

Bo Persson
sumber
10
@LuchianGrigore: UB bukanlah standar yang dikatakan UB, melainkan standar yang tidak menggambarkan bagaimana seharusnya bekerja. Ini persis seperti itu. Apakah standar menggambarkan apa yang terjadi? Apakah dikatakan implementasi itu sudah ditentukan? Tidak dan tidak Jadi itu UB. Selain itu, mengenai argumen "anggota berbagi alamat memori yang sama", Anda harus merujuk pada aturan aliasing, yang akan membawa Anda ke UB lagi.
Yakov Galka
5
@Luchian: Cukup jelas apa arti aktif, "yaitu, nilai paling banyak dari anggota data non-statis dapat disimpan dalam serikat kapan saja."
Benjamin Lindley
5
@LuchianGrigore: Ya ada. Ada jumlah kasus tak terbatas yang tidak ditangani oleh standar (dan tidak bisa). (C ++ adalah VM lengkap Turing sehingga tidak lengkap.) Jadi apa? Itu menjelaskan apa yang dimaksud "aktif", merujuk pada kutipan di atas, setelah "itu adalah".
Yakov Galka
8
@LuchianGrigore: Penghilangan definisi perilaku yang eksplisit juga merupakan perilaku tidak terdefinisi yang tidak dipertimbangkan, menurut bagian definisi.
jxh
5
@Claudiu Itu UB karena alasan yang berbeda - itu melanggar alias ketat.
Mysticial
18

Saya pikir yang paling mendekati standar untuk mengatakan itu perilaku tidak terdefinisi adalah di mana ia mendefinisikan perilaku untuk serikat pekerja yang berisi urutan awal umum (C99, §6.5.2.3 / 5):

Satu jaminan khusus dibuat untuk menyederhanakan penggunaan serikat: jika serikat pekerja mengandung beberapa struktur yang memiliki urutan awal yang sama (lihat di bawah), dan jika objek serikat pekerja saat ini mengandung salah satu dari struktur ini, maka diizinkan untuk memeriksa umum bagian awal dari salah satu dari mereka di mana saja bahwa deklarasi jenis lengkap serikat terlihat. Dua struktur berbagi urutan awal yang sama jika anggota yang sesuai memiliki tipe yang kompatibel (dan, untuk bidang bit, lebar yang sama) untuk urutan satu atau lebih anggota awal.

C ++ 11 memberikan persyaratan / izin yang sama di §9.2 / 19:

Jika gabungan tata letak standar berisi dua atau lebih struktur tata letak standar yang berbagi urutan awal yang sama, dan jika objek gabungan tata letak standar saat ini berisi salah satu struktur tata letak standar ini, maka diizinkan untuk memeriksa bagian awal umum dari setiap dari mereka. Dua struktur tata letak standar berbagi urutan awal yang sama jika anggota yang sesuai memiliki tipe yang kompatibel dengan tata letak dan tidak satu pun anggota adalah bidang-bit atau keduanya adalah bidang-bit dengan lebar yang sama untuk urutan satu atau lebih anggota awal.

Meskipun tidak ada yang menyatakan secara langsung, keduanya membawa implikasi yang kuat bahwa "memeriksa" (membaca) anggota "diizinkan" hanya jika 1) itu adalah (bagian dari) anggota yang paling baru ditulis, atau 2) merupakan bagian dari inisial umum urutan.

Itu bukan pernyataan langsung bahwa melakukan sebaliknya adalah perilaku yang tidak terdefinisi, tetapi yang paling dekat yang saya sadari.

Jerry Coffin
sumber
Untuk membuatnya lengkap, Anda perlu tahu apa "tipe yang kompatibel dengan tata letak" untuk C ++, atau "tipe yang kompatibel" untuk C.
Michael Anderson
2
@MichaelAnderson: Ya dan tidak. Anda perlu berurusan dengan itu ketika / jika Anda ingin memastikan apakah sesuatu termasuk dalam pengecualian ini - tetapi pertanyaan sebenarnya di sini adalah apakah sesuatu yang jelas berada di luar pengecualian benar-benar memberi UB. Saya pikir itu cukup kuat tersirat di sini untuk membuat maksudnya jelas, tetapi saya tidak berpikir itu pernah dinyatakan secara langsung.
Jerry Coffin
Hal "urutan awal umum" ini mungkin saja menyelamatkan 2 atau 3 proyek saya dari Bin Penulisan Ulang. Saya sangat marah ketika pertama kali membaca tentang sebagian besar penggunaan hukuman unionyang tidak terdefinisi, karena saya mendapat kesan oleh blog tertentu bahwa ini tidak masalah, dan membangun beberapa struktur besar dan proyek di sekitarnya. Sekarang saya pikir saya mungkin baik-baik saja, karena kelas saya unionmemang berisi kelas yang memiliki tipe yang sama di depan
underscore_d
@JerryCoffin, saya pikir Anda sedang mengisyaratkan pertanyaan yang sama seperti saya: bagaimana jika kita unionmengandung misalnya sebuah uint8_tdan class Something { uint8_t myByte; [...] };- saya akan berasumsi ketentuan ini juga akan berlaku di sini, tapi itu bernada sangat sengaja hanya memungkinkan structs. Untungnya saya sudah menggunakan yang bukan primitif mentah: O
underscore_d
@underscore_d: Standar C setidaknya semacam mencakup pertanyaan itu: "Sebuah penunjuk ke objek struktur, yang dikonversi dengan tepat, menunjuk ke anggota awalnya (atau jika anggota itu adalah bidang-bit, kemudian ke unit tempat ia berada) , dan sebaliknya."
Jerry Coffin
12

Sesuatu yang belum disebutkan oleh jawaban yang tersedia adalah catatan kaki 37 dalam paragraf 21 bagian 6.2.5:

Perhatikan bahwa tipe agregat tidak termasuk tipe union karena objek dengan tipe union hanya dapat berisi satu anggota pada suatu waktu.

Persyaratan ini tampaknya secara jelas menyiratkan bahwa Anda tidak boleh menulis di anggota dan membaca yang lain. Dalam hal ini mungkin perilaku yang tidak terdefinisi dengan kurangnya spesifikasi.

mpu
sumber
Banyak implementasi mendokumentasikan format penyimpanan dan aturan tata letak mereka. Spesifikasi semacam itu dalam banyak kasus akan menyiratkan apa efek dari penyimpanan baca dari satu jenis dan penulisan yang lain karena tidak adanya aturan yang mengatakan kompiler tidak harus benar-benar menggunakan format penyimpanan yang ditentukan kecuali ketika hal-hal dibaca dan ditulis menggunakan pointer. dari tipe karakter.
supercat
-3

Saya jelaskan ini dengan sebuah contoh.
menganggap kita memiliki kesatuan berikut:

union A{
   int x;
   short y[2];
};

Saya berasumsi bahwa sizeof(int)memberi 4, dan itu sizeof(short)memberi 2.
ketika Anda menulis dengan union A a = {10}baik buat var baru tipe A di dalamnya nilai 10.

memori Anda akan terlihat seperti itu: (ingat bahwa semua anggota serikat pekerja mendapatkan lokasi yang sama)

       | x |
       | y [0] | y [1] |
       -----------------------------------------
   a-> | 0000 0000 | 0000 0000 | 0000 0000 | 0000 1010 |
       -----------------------------------------

seperti yang Anda lihat, nilai kapak adalah 10, nilai ay 1 adalah 10, dan nilai ay [0] adalah 0.

sekarang, apa yang terjadi jika saya melakukan ini?

a.y[0] = 37;

ingatan kita akan terlihat seperti ini:

       | x |
       | y [0] | y [1] |
       -----------------------------------------
   a-> | 0000 0000 | 0010 0101 | 0000 0000 | 0000 1010 |
       -----------------------------------------

ini akan mengubah nilai kapak menjadi 2424842 (dalam desimal).

sekarang, jika serikat Anda memiliki float, atau double, peta memori Anda menjadi lebih berantakan, karena cara Anda menyimpan angka pastinya. info lebih lanjut bisa Anda dapatkan di sini .

elyashiv
sumber
18
:) Ini bukan yang saya minta. Saya tahu apa yang terjadi secara internal. Saya tahu itu berhasil. Saya bertanya apakah itu dalam standar.
Luchian Grigore