Terlepas dari seberapa 'buruk' kode tersebut, dan dengan asumsi bahwa penyelarasan dll bukanlah masalah pada kompiler / platform, apakah ini perilaku yang tidak terdefinisi atau rusak?
Jika saya memiliki struct seperti ini: -
struct data
{
int a, b, c;
};
struct data thing;
Apakah hukum untuk mengakses a
, b
dan c
sebagai (&thing.a)[0]
, (&thing.a)[1]
, dan (&thing.a)[2]
?
Dalam setiap kasus, pada setiap kompiler dan platform saya mencobanya, dengan setiap pengaturan saya mencobanya 'bekerja'. Saya hanya khawatir bahwa kompilator mungkin tidak menyadari bahwa b dan thing [1] adalah hal yang sama dan penyimpanan ke 'b' mungkin dimasukkan ke dalam register dan thing [1] membaca nilai yang salah dari memori (misalnya). Dalam setiap kasus yang saya coba lakukan hal yang benar. (Saya menyadari tentu saja itu tidak membuktikan banyak)
Ini bukan kode saya; itu kode yang harus saya tangani , saya tertarik apakah ini kode yang buruk atau kode rusak karena perbedaannya memengaruhi prioritas saya untuk banyak mengubahnya :)
Diberi tag C dan C ++. Saya kebanyakan tertarik pada C ++ tetapi juga C jika berbeda, hanya untuk minat.
Jawaban:
Itu ilegal 1 . Itu adalah perilaku yang tidak ditentukan di C ++.
Anda mengambil anggota dalam gaya array, tetapi inilah yang dikatakan standar C ++ (penekanan saya):
Namun, untuk anggota, tidak ada persyaratan yang berdekatan :
Meskipun dua tanda kutip di atas seharusnya cukup untuk memberi petunjuk mengapa pengindeksan menjadi
struct
seperti yang Anda lakukan bukanlah perilaku yang ditentukan oleh standar C ++, mari kita pilih satu contoh: lihat ekspresi(&thing.a)[2]
- Mengenai operator subskrip:Menggali teks tebal dari kutipan di atas: tentang menambahkan tipe integral ke tipe penunjuk (perhatikan penekanannya di sini) ..
Perhatikan persyaratan larik untuk klausa if ; lain sebaliknya dalam kutipan di atas. Ekspresi tersebut
(&thing.a)[2]
jelas tidak memenuhi syarat untuk klausa if ; Karenanya, Perilaku Tidak Terdefinisi.Di samping catatan: Meskipun saya telah bereksperimen secara ekstensif kode dan variasinya pada berbagai kompiler dan mereka tidak memperkenalkan padding apa pun di sini, ( berhasil ); dari sudut pandang pemeliharaan, kode ini sangat rapuh. Anda masih harus menegaskan bahwa implementasi mengalokasikan anggota secara berdekatan sebelum melakukan ini. Dan tetap terikat :-). Tapi perilakunya masih belum ditentukan ....
Beberapa solusi yang layak (dengan perilaku yang ditentukan) telah disediakan oleh jawaban lain.
Seperti yang ditunjukkan dengan benar di komentar, [basic.lval / 8] , yang saya edit sebelumnya tidak berlaku. Terima kasih @ 2501 dan @MM
1 : Lihat jawaban @ Barry atas pertanyaan ini untuk satu-satunya kasus hukum di mana Anda dapat mengakses
thing.a
anggota struct melalui parttern ini.sumber
- an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
Tidak. Di C, ini adalah perilaku yang tidak terdefinisi meskipun tidak ada padding.
Hal yang menyebabkan perilaku tidak terdefinisi adalah akses di luar batas 1 . Ketika Anda memiliki skalar (anggota a, b, c di struct) dan mencoba menggunakannya sebagai larik 2 untuk mengakses elemen hipotetis berikutnya, Anda menyebabkan perilaku tidak terdefinisi, bahkan jika kebetulan ada objek lain dengan tipe yang sama di alamat itu.
Namun Anda dapat menggunakan alamat objek struct dan menghitung offset menjadi anggota tertentu:
Ini harus dilakukan untuk setiap anggota secara individual, tetapi dapat dimasukkan ke dalam fungsi yang menyerupai akses array.
1 (Dikutip dari: ISO / IEC 9899: 201x 6.5.6 Operator aditif 8)
Jika hasil menunjuk satu melewati elemen terakhir dari objek array, itu tidak boleh digunakan sebagai operand dari operator unary * yang dievaluasi.
2 (Dikutip dari: ISO / IEC 9899: 201x 6.5.6 Operator aditif 7)
Untuk keperluan operator ini, penunjuk ke objek yang bukan elemen larik berperilaku sama seperti penunjuk ke elemen pertama dari sebuah array dengan panjang satu dengan tipe objek sebagai tipe elemennya.
sumber
char* p = ( char* )&thing.a + offsetof( thing , b );
mengarah pada perilaku yang tidak terdefinisi?Di C ++ jika Anda benar-benar membutuhkannya - buat operator []:
ini tidak hanya dijamin berfungsi tetapi penggunaannya lebih sederhana, Anda tidak perlu menulis ekspresi yang tidak dapat dibaca
(&thing.a)[0]
Catatan: jawaban ini diberikan dengan asumsi Anda sudah memiliki struktur dengan bidang, dan Anda perlu menambahkan akses melalui indeks. Jika kecepatan menjadi masalah dan Anda dapat mengubah strukturnya, ini bisa menjadi lebih efektif:
Solusi ini akan mengubah ukuran struktur sehingga Anda dapat menggunakan metode juga:
sumber
thing.a()
.Untuk c ++: Jika Anda perlu mengakses anggota tanpa mengetahui namanya, Anda dapat menggunakan penunjuk ke variabel anggota.
sumber
offsetoff
dalam C.Dalam ISO C99 / C11, jenis-punning berbasis union adalah legal, jadi Anda dapat menggunakannya daripada mengindeks pointer ke non-array (lihat berbagai jawaban lain).
ISO C ++ tidak mengizinkan jenis punning berbasis gabungan. GNU C ++ memang, sebagai ekstensi , dan saya pikir beberapa kompiler lain yang tidak mendukung ekstensi GNU secara umum mendukung union type-punning. Tetapi itu tidak membantu Anda menulis kode yang sangat portabel.
Dengan versi gcc dan clang saat ini, menulis fungsi anggota C ++ menggunakan a
switch(idx)
untuk memilih anggota akan mengoptimalkan indeks konstan waktu kompilasi, tetapi akan menghasilkan asm bercabang yang mengerikan untuk indeks waktu proses. Tidak ada yang salah denganswitch()
hal ini; ini hanyalah bug pengoptimalan yang terlewat di kompiler saat ini. Mereka bisa mengkompilasi fungsi switch () Slava secara efisien.Solusi / solusi untuk ini adalah melakukannya dengan cara lain: berikan kelas / struct Anda anggota array, dan tulis fungsi pengakses untuk melampirkan nama ke elemen tertentu.
Kita dapat melihat keluaran asm untuk kasus penggunaan yang berbeda, pada penjelajah kompilator Godbolt . Ini adalah fungsi Sistem V x86-64 lengkap, dengan instruksi RET tambahan dihilangkan untuk lebih menunjukkan apa yang Anda dapatkan ketika mereka sebaris. ARM / MIPS / apa pun yang serupa.
Sebagai perbandingan, jawaban @ Slava menggunakan a
switch()
for C ++ membuat asm seperti ini untuk indeks variabel runtime. (Kode di tautan Godbolt sebelumnya).Ini jelas mengerikan, dibandingkan dengan versi pelesetan tipe berbasis serikat C (atau GNU C ++):
sumber
[]
operator secara langsung pada anggota serikat, Standar mendefinisikanarray[index]
sebagai setara dengan*((array)+(index))
, dan baik gcc maupun clang tidak akan dapat diandalkan mengenali bahwa akses ke*((someUnion.array)+(index))
adalah akses kesomeUnion
. Satu-satunya penjelasan yang bisa saya lihat adalah bahwasomeUnion.array[index]
tidak*((someUnion.array)+(index))
tidak didefinisikan oleh Standar, tetapi hanya sebuah ekstensi populer, dan gcc / dentang telah memilih untuk tidak mendukung kedua tetapi tampaknya mendukung yang pertama, setidaknya untuk saat ini.Di C ++, ini sebagian besar perilaku yang tidak ditentukan (tergantung indeks mana).
Dari [expr.unary.op]:
Dengan
&thing.a
demikian, ekspresi tersebut dianggap merujuk ke larik satuint
.Dari [expr.sub]:
Dan dari [expr.add]:
(&thing.a)[0]
terbentuk sempurna karena&thing.a
dianggap sebagai larik berukuran 1 dan kami mengambil indeks pertama tersebut. Itu adalah indeks yang diizinkan untuk diambil.(&thing.a)[2]
melanggar prasyarat bahwa0 <= i + j <= n
, karena kita memilikii == 0
,j == 2
,n == 1
. Cukup membuat penunjuk&thing.a + 2
adalah perilaku yang tidak ditentukan.(&thing.a)[1]
adalah kasus yang menarik. Itu sebenarnya tidak melanggar apa pun di [expr.add]. Kami diizinkan untuk mengambil penunjuk satu melewati akhir larik - yang ini akan terjadi. Di sini, kita beralih ke catatan di [basic.compound]:Oleh karena itu, mengambil penunjuk
&thing.a + 1
adalah perilaku yang didefinisikan, tetapi mendereferensi itu tidak ditentukan karena tidak menunjuk ke apa pun.sumber
(&thing.a + 1)
adalah kasus menarik yang gagal saya bahas. +1! ... Cuma penasaran, apakah Anda termasuk dalam komite ISO C ++?Ini adalah perilaku yang tidak terdefinisi.
Ada banyak aturan dalam C ++ yang mencoba memberi kompiler harapan untuk memahami apa yang Anda lakukan, sehingga dapat mempertimbangkannya dan mengoptimalkannya.
Ada aturan tentang aliasing (mengakses data melalui dua jenis penunjuk yang berbeda), batas array, dll.
Jika Anda memiliki variabel
x
, fakta bahwa variabel tersebut bukan anggota array berarti kompilator dapat berasumsi bahwa tidak ada[]
akses array berbasis yang dapat mengubahnya. Jadi tidak harus terus-menerus memuat ulang data dari memori setiap kali Anda menggunakannya; hanya jika seseorang bisa mengubahnya dari namanya .Dengan demikian
(&thing.a)[1]
dapat diasumsikan oleh compiler untuk tidak merujukthing.b
. Ia dapat menggunakan fakta ini untuk menyusun ulang baca dan tulis kething.b
, membatalkan apa yang Anda ingin lakukan tanpa membatalkan apa yang sebenarnya Anda perintahkan.Contoh klasik dari ini adalah membuang const.
di sini Anda biasanya mendapatkan kompiler yang mengatakan 7 lalu 2! = 7, dan kemudian dua petunjuk identik; terlepas dari fakta yang
ptr
menunjukx
. Kompilator menganggap fakta bahwax
nilai konstan tidak perlu repot-repot membacanya saat Anda meminta nilaix
.Tetapi ketika Anda mengambil alamat
x
, Anda memaksanya untuk ada. Anda kemudian membuang const, dan memodifikasinya. Jadi lokasi sebenarnya dalam memorix
yang telah dimodifikasi, compiler bebas untuk tidak benar-benar membacanya saat membacax
!Kompiler mungkin menjadi cukup pintar untuk mencari tahu bagaimana menghindari mengikuti
ptr
untuk membaca*ptr
, tetapi seringkali tidak. Silakan pergi dan gunakanptr = ptr+argc-1
atau kebingungan seperti itu jika pengoptimal semakin pintar dari Anda.Anda bisa memberikan kebiasaan
operator[]
yang mendapatkan barang yang tepat.memiliki keduanya berguna.
sumber
(&thing.a)[0]
dapat memodifikasinyax
karena ia tahu Anda tidak dapat mengubahnya dengan cara yang ditentukan. Pengoptimalan serupa dapat terjadi ketika Anda mengubahb
melalui(&blah.a)[1]
jika kompilator dapat membuktikan tidak ada akses yang ditentukanb
yang dapat mengubahnya; perubahan seperti itu dapat terjadi karena perubahan yang tampaknya tidak berbahaya pada kompilator, kode sekitarnya, atau apa pun. Jadi, bahkan pengujian yang berhasil saja tidak cukup.Inilah cara untuk menggunakan kelas proxy untuk mengakses elemen dalam array anggota dengan nama. Ini sangat C ++, dan tidak memiliki manfaat vs. fungsi aksesor yang mengembalikan ref, kecuali untuk preferensi sintaksis. Ini membebani
->
operator untuk mengakses elemen sebagai anggota, jadi agar dapat diterima, seseorang harus tidak menyukai sintaksis aksesor (d.a() = 5;
), serta mentolerir penggunaan->
dengan objek non-pointer. Saya berharap ini mungkin juga membingungkan pembaca yang tidak terbiasa dengan kode, jadi ini mungkin lebih merupakan trik rapi daripada sesuatu yang ingin Anda masukkan ke dalam produksi.The
Data
struct dalam kode ini juga termasuk overloads untuk operator subscript, untuk elemen akses diindeks dalam yangar
anggota array, sertabegin
danend
fungsi, untuk iterasi. Juga, semua ini kelebihan beban dengan versi non-const dan const, yang menurut saya perlu disertakan untuk kelengkapan.Ketika
Data
's->
digunakan untuk mengakses elemen dengan nama (seperti ini:my_data->b = 5;
), sebuahProxy
objek dikembalikan. Kemudian, karena nilaiProxy
r ini bukan penunjuk,->
operatornya sendiri disebut rantai otomatis, yang mengembalikan penunjuk ke dirinya sendiri. Dengan cara ini,Proxy
objek dibuat instance-nya dan tetap valid selama evaluasi ekspresi awal.Konstruksi
Proxy
objek mengisi 3 anggota referensinyaa
,b
danc
menurut pointer yang diteruskan dalam konstruktor, yang diasumsikan mengarah ke buffer yang berisi setidaknya 3 nilai yang tipenya diberikan sebagai parameter templateT
. Jadi alih-alih menggunakan referensi bernama yang merupakan anggotaData
kelas, ini menghemat memori dengan mengisi referensi pada titik akses (tapi sayangnya, menggunakan->
dan bukan.
operator).Untuk menguji seberapa baik pengoptimal compiler menghilangkan semua tipu muslihat yang diperkenalkan oleh penggunaan
Proxy
, kode di bawah ini menyertakan 2 versimain()
. The#if 1
Versi menggunakan->
dan[]
operator, dan#if 0
Melakukan versi setara set prosedur, tetapi hanya dengan langsung mengaksesData::ar
.The
Nci()
Fungsi menghasilkan nilai integer runtime untuk menginisialisasi elemen array, yang mencegah optimizer dari hanya memasukkan nilai-nilai konstan langsung ke masing-masingstd::cout
<<
panggilan.Untuk gcc 6.2, menggunakan -O3, kedua versi
main()
menghasilkan rakitan yang sama (beralih antara#if 1
dan#if 0
sebelum yang pertamamain()
untuk membandingkan): https://godbolt.org/g/QqRWZbsumber
main()
dengan fungsi pengaturan waktu! misalnyaint getb(Data *d) { return (*d)->b; }
mengkompilasi ke hanyamov eax, DWORD PTR [rdi+4]
/ret
( godbolt.org/g/89d3Np ). (Ya,Data &d
akan membuat sintaksnya lebih mudah, tetapi saya menggunakan penunjuk alih-alih ref untuk menyoroti keanehan kelebihan beban->
dengan cara ini.)int tmp[] = { a, b, c}; return tmp[idx];
tidak dioptimalkan, jadi rapi yang ini bisa.operator.
di C ++ 17.Jika membaca nilai sudah cukup, dan efisiensi bukan masalah, atau jika Anda memercayai kompiler Anda untuk mengoptimalkan semuanya dengan baik, atau jika struct hanya 3 byte, Anda dapat melakukan ini dengan aman:
Untuk versi C ++ saja, Anda mungkin ingin menggunakan
static_assert
untuk memverifikasi bahwastruct data
memiliki tata letak standar, dan mungkin melemparkan pengecualian pada indeks yang tidak valid.sumber
Ini ilegal, tetapi ada solusi lain:
Sekarang Anda dapat mengindeks v:
sumber