Apakah legal untuk mengindeks ke dalam struct?


104

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, bdan csebagai (&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.


51
Tidak, ini tidak "legal". Ini adalah perilaku yang tidak terdefinisi.
Sam Varshavchik

10
Ini berfungsi untuk Anda dalam kasus yang sangat sederhana ini karena kompiler tidak menambahkan padding apa pun di antara anggota. Coba dengan struktur menggunakan tipe ukuran berbeda dan akan runtuh.
Beberapa programmer dude

7
Menggali masa lalu - UB dulu dijuluki daemon nasal .
Adrian Colomitchi

21
Hebat, di sini saya tersandung karena saya mengikuti tag C, membaca pertanyaan, lalu menulis jawaban yang hanya berlaku untuk C, karena saya tidak melihat tag C ++. C dan C ++ sangat berbeda di sini! C memungkinkan jenis punning dengan serikat pekerja, C ++ tidak.
Lundin

7
Jika Anda perlu mengakses elemen sebagai larik, tentukan sebagai larik. Jika mereka perlu memiliki nama yang berbeda, gunakan nama tersebut. Mencoba memiliki kue dan memakannya pada akhirnya akan menyebabkan gangguan pencernaan - mungkin pada waktu yang paling tidak nyaman. (Saya pikir indeks 0 legal di C; indeks 1 atau 2 tidak. Ada konteks di mana satu elemen diperlakukan sebagai larik ukuran 1.)
Jonathan Leffler

Jawaban:


73

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):

[dcl.array / 1] : ... Objek bertipe array berisikumpulan N subobjek tipe T yang tidak kosong dan dialokasikan secara berdekatan ...

Namun, untuk anggota, tidak ada persyaratan yang berdekatan :

[class.mem / 17] : ...; Persyaratan penyelarasan implementasi dapat menyebabkan dua anggota yang berdekatan tidak dialokasikan segera setelah satu sama lain ...

Meskipun dua tanda kutip di atas seharusnya cukup untuk memberi petunjuk mengapa pengindeksan menjadi structseperti yang Anda lakukan bukanlah perilaku yang ditentukan oleh standar C ++, mari kita pilih satu contoh: lihat ekspresi (&thing.a)[2]- Mengenai operator subskrip:

[expr.post//expr.sub/1] : Ekspresi postfix yang diikuti dengan ekspresi dalam tanda kurung siku adalah ekspresi postfix. Salah satu ekspresi harus menjadi nilai gl dari tipe "larik T" atau nilai awal dari tipe "penunjuk ke T" dan yang lainnya harus menjadi nilai awal dari pencacahan tanpa batas atau tipe integral. Hasilnya adalah tipe "T". Tipe "T" haruslah tipe objek yang sepenuhnya terdefinisi.66 Ekspresi E1[E2]ini identik (menurut definisi)((E1)+(E2))

Menggali teks tebal dari kutipan di atas: tentang menambahkan tipe integral ke tipe penunjuk (perhatikan penekanannya di sini) ..

[expr.add / 4] : Jika ekspresi yang memiliki tipe integral ditambahkan atau dikurangkan dari pointer, hasilnya memiliki tipe operan pointer. Jika ekspresiPpoin untuk elemenx[i]dari array objekx dengan n elemen, ekspresiP + JdanJ + P(di manaJmemiliki nilaij) titik ke (mungkin-hipotetis) elemenx[i + j] jika0 ≤ i + j ≤ n; jika tidak , perilaku tidak terdefinisi. ...

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.aanggota struct melalui parttern ini.


1
@jcoder Ini didefinisikan di class.mem . Lihat paragraf terakhir untuk teks sebenarnya.
NathanOliver

4
Alisasi yang ketat tidak relevan di sini. Tipe int terdapat di dalam tipe agregat dan tipe ini mungkin alias int. - 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),
2501

1
@ Para downvoters, mau berkomentar? - dan untuk meningkatkan atau menunjukkan di mana jawaban ini salah?
WhiZTiM

4
Aliasing ketat tidak relevan dengan ini. Padding bukanlah bagian dari nilai tersimpan suatu objek. Juga jawaban ini gagal untuk menjawab kasus yang paling umum: apa yang terjadi bila tidak ada padding. Akan merekomendasikan menghapus jawaban ini sebenarnya.
MM

1
Selesai! Saya telah menghapus paragraf tentang strict-aliasing.
WhiZTiM

48

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:

struct data thing = { 0 };
char* p = ( char* )&thing + offsetof( thing , b );
int* b = ( int* )p;
*b = 123;
assert( thing.b == 123 );

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.


3
Perhatikan ini hanya berfungsi jika kelasnya adalah tipe tata letak standar. Kalau belum masih UB.
NathanOliver

@NathanOliver Saya harus menyebutkan bahwa jawaban saya hanya berlaku untuk C. Diedit. Ini adalah salah satu masalah dari pertanyaan bahasa tag ganda.
2501

Terima kasih, dan itulah mengapa saya meminta C ++ dan C secara terpisah karena menarik untuk mengetahui perbedaannya
jcoder

@NathanOliver Alamat anggota pertama dijamin sama dengan alamat kelas C ++ jika layout standarnya. Namun, itu tidak menjamin bahwa akses didefinisikan dengan baik atau menyiratkan bahwa akses tersebut pada kelas lain tidak ditentukan.
Potatoswatter

apakah menurut Anda itu char* p = ( char* )&thing.a + offsetof( thing , b );mengarah pada perilaku yang tidak terdefinisi?
MM

43

Di C ++ jika Anda benar-benar membutuhkannya - buat operator []:

struct data
{
    int a, b, c;
    int &operator[]( size_t idx ) {
        switch( idx ) {
            case 0 : return a;
            case 1 : return b;
            case 2 : return c;
            default: throw std::runtime_error( "bad index" );
        }
    }
};


data d;
d[0] = 123; // assign 123 to data.a

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:

struct data 
{
     int array[3];
     int &a = array[0];
     int &b = array[1];
     int &c = array[2];
};

Solusi ini akan mengubah ukuran struktur sehingga Anda dapat menggunakan metode juga:

struct data 
{
     int array[3];
     int &a() { return array[0]; }
     int &b() { return array[1]; }
     int &c() { return array[2]; }
};

1
Saya ingin melihat pembongkaran ini, versus pembongkaran program C menggunakan jenis punning. Tapi, tapi ... C ++ secepat C ... kan? Baik?
Lundin

6
@ Lundin jika Anda peduli dengan kecepatan konstruksi ini maka data harus diatur sebagai larik terlebih dahulu, bukan sebagai bidang terpisah.
Slava

2
@ Lundin maksudnya Perilaku tak terbaca dan tak terdefinisi? Tidak, terima kasih.
Slava

1
@Lundin Operator overloading adalah fitur sintaksis waktu kompilasi yang tidak menyebabkan overhead apapun dibandingkan dengan fungsi normal. Lihatlah godbolt.org/g/vqhREz untuk melihat apa yang sebenarnya dilakukan kompilator ketika mengkompilasi kode C ++ dan C. Sungguh menakjubkan apa yang mereka lakukan dan apa yang diharapkan dari mereka. Saya pribadi lebih suka keamanan tipe dan ekspresi C ++ yang lebih baik daripada C jutaan kali. Dan itu bekerja sepanjang waktu tanpa bergantung pada asumsi tentang padding.
Jens

2
Referensi tersebut setidaknya akan menggandakan ukuran benda tersebut. Lakukan saja thing.a().
TC

14

Untuk c ++: Jika Anda perlu mengakses anggota tanpa mengetahui namanya, Anda dapat menggunakan penunjuk ke variabel anggota.

struct data {
  int a, b, c;
};

typedef int data::* data_int_ptr;

data_int_ptr arr[] = {&data::a, &data::b, &data::c};

data thing;
thing.*arr[0] = 123;

1
Ini menggunakan fasilitas bahasa, dan sebagai hasilnya didefinisikan dengan baik dan, seperti yang saya asumsikan, efisien. Jawaban Terbaik.
Peter - Pulihkan Monica

2
Anggap efisien? Saya berasumsi sebaliknya. Lihat kode yang dihasilkan.
JDługosz

1
@ JDługosz, Anda benar. Mengambil mengintip di dihasilkan perakitan, tampaknya gcc 6.2 menciptakan kode setara dengan menggunakan offsetoffdalam C.
StoryTeller - Unslander Monica

3
Anda juga dapat meningkatkan kualitas dengan membuat arr constexpr. Ini akan membuat tabel pencarian tetap tunggal di bagian data daripada membuatnya dengan cepat.
Tim

10

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 dengan switch()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.

struct array_data
{
  int arr[3];

  int &operator[]( unsigned idx ) {
      // assert(idx <= 2);
      //idx = (idx > 2) ? 2 : idx;
      return arr[idx];
  }
  int &a(){ return arr[0]; } // TODO: const versions
  int &b(){ return arr[1]; }
  int &c(){ return arr[2]; }
};

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.

# asm from g++6.2 -O3
int getb(array_data &d) { return d.b(); }
    mov     eax, DWORD PTR [rdi+4]

void setc(array_data &d, int val) { d.c() = val; }
    mov     DWORD PTR [rdi+8], esi

int getidx(array_data &d, int idx) { return d[idx]; }
    mov     esi, esi                   # zero-extend to 64-bit
    mov     eax, DWORD PTR [rdi+rsi*4]

Sebagai perbandingan, jawaban @ Slava menggunakan a switch()for C ++ membuat asm seperti ini untuk indeks variabel runtime. (Kode di tautan Godbolt sebelumnya).

int cpp(data *d, int idx) {
    return (*d)[idx];
}

    # gcc6.2 -O3, using `default: __builtin_unreachable()` to promise the compiler that idx=0..2,
    # avoiding an extra cmov for idx=min(idx,2), or an extra branch to a throw, or whatever
    cmp     esi, 1
    je      .L6
    cmp     esi, 2
    je      .L7
    mov     eax, DWORD PTR [rdi]
    ret
.L6:
    mov     eax, DWORD PTR [rdi+4]
    ret
.L7:
    mov     eax, DWORD PTR [rdi+8]
    ret

Ini jelas mengerikan, dibandingkan dengan versi pelesetan tipe berbasis serikat C (atau GNU C ++):

c(type_t*, int):
    movsx   rsi, esi                   # sign-extend this time, since I didn't change idx to unsigned here
    mov     eax, DWORD PTR [rdi+rsi*4]

@ MM: poin bagus. Ini lebih merupakan jawaban untuk berbagai komentar, dan alternatif jawaban Slava. Saya mengulang kata-kata pembukaannya, jadi ini setidaknya dimulai sebagai jawaban untuk pertanyaan awal. Terima kasih telah menunjukkannya.
Peter Cordes

Sementara jenis punning berbasis serikat tampaknya berfungsi di gcc dan clang saat menggunakan []operator secara langsung pada anggota serikat, Standar mendefinisikan array[index]sebagai setara dengan *((array)+(index)), dan baik gcc maupun clang tidak akan dapat diandalkan mengenali bahwa akses ke *((someUnion.array)+(index))adalah akses ke someUnion. Satu-satunya penjelasan yang bisa saya lihat adalah bahwa someUnion.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.
supercat

9

Di C ++, ini sebagian besar perilaku yang tidak ditentukan (tergantung indeks mana).

Dari [expr.unary.op]:

Untuk keperluan aritmatika pointer (5.7) dan perbandingan (5.9, 5.10), sebuah objek yang bukan elemen array yang alamatnya diambil dengan cara ini dianggap milik array dengan satu elemen tipe T.

Dengan &thing.ademikian, ekspresi tersebut dianggap merujuk ke larik satu int.

Dari [expr.sub]:

Ekspresi E1[E2]tersebut identik (menurut definisi) dengan*((E1)+(E2))

Dan dari [expr.add]:

Ketika ekspresi yang memiliki tipe integral ditambahkan ke atau dikurangkan dari pointer, hasilnya memiliki tipe operan pointer. Jika ekspresi Pmenunjuk ke elemen x[i]objek larik xdengan nelemen, ekspresi P + Jdan J + P(di mana Jmemiliki nilai j) mengarah ke elemen (kemungkinan hipotetis) x[i + j]jika0 <= i + j <= n ; jika tidak, perilaku tidak terdefinisi.

(&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 bahwa 0 <= i + j <= n, karena kita memiliki i == 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]:

Nilai tipe penunjuk yang merupakan penunjuk ke atau melewati ujung suatu objek mewakili alamat byte pertama dalam memori (1.7) yang ditempati oleh objek53 atau byte pertama dalam memori setelah akhir penyimpanan yang ditempati oleh objek , masing-masing. [Catatan: Sebuah pointer yang melewati akhir suatu objek (5.7) tidak dianggap menunjuk ke objek yang tidak terkait dari tipe objek yang mungkin terletak di alamat itu.

Oleh karena itu, mengambil penunjuk &thing.a + 1adalah perilaku yang didefinisikan, tetapi mendereferensi itu tidak ditentukan karena tidak menunjuk ke apa pun.


Mengevaluasi (& thing.a) +1 hampir legal karena penunjuk melewati akhir larik adalah sah; membaca atau menulis data yang disimpan ada perilaku yang tidak terdefinisi, membandingkan dengan & thing.b dengan <,>, <=,> = adalah perilaku tidak terdefinisi. (& thing.a) + 2 benar-benar ilegal.
gnasher729

@ gnasher729 Ya, ada baiknya mengklarifikasi jawabannya lagi.
Barry

Ini (&thing.a + 1)adalah kasus menarik yang gagal saya bahas. +1! ... Cuma penasaran, apakah Anda termasuk dalam komite ISO C ++?
WhiZTiM

Ini juga kasus yang sangat penting karena jika tidak setiap loop yang menggunakan pointer sebagai interval setengah terbuka akan menjadi UB.
Jens

Mengenai kutipan standar terakhir. C ++ harus ditentukan lebih baik daripada C di sini.
2501

8

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 merujuk thing.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.

const int x = 7;
std::cout << x << '\n';
auto ptr = (int*)&x;
*ptr = 2;
std::cout << *ptr << "!=" << x << '\n';
std::cout << ptr << "==" << &x << '\n';

di sini Anda biasanya mendapatkan kompiler yang mengatakan 7 lalu 2! = 7, dan kemudian dua petunjuk identik; terlepas dari fakta yang ptrmenunjuk x. Kompilator menganggap fakta bahwa xnilai 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 memori xyang telah dimodifikasi, compiler bebas untuk tidak benar-benar membacanya saat membacax !

Kompiler mungkin menjadi cukup pintar untuk mencari tahu bagaimana menghindari mengikuti ptruntuk 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.

int& operator[](std::size_t);
int const& operator[](std::size_t) const;

memiliki keduanya berguna.


"fakta bahwa ini bukan anggota dari sebuah array berarti bahwa kompilator dapat berasumsi bahwa tidak ada akses array berbasis [] yang dapat mengubahnya." - tidak benar, misalnya (&thing.a)[0]dapat memodifikasinya
MM

Saya tidak melihat bagaimana contoh const ada hubungannya dengan pertanyaan tersebut. Itu gagal hanya karena ada aturan khusus bahwa objek const tidak boleh dimodifikasi, bukan alasan lain.
MM

1
@MM, ini bukan contoh pengindeksan ke dalam struct, tetapi ini adalah ilustrasi yang sangat bagus tentang bagaimana menggunakan perilaku tidak terdefinisi untuk mereferensikan sesuatu dengan lokasinya yang terlihat di memori, dapat menghasilkan keluaran yang berbeda dari yang diharapkan, karena kompilator dapat melakukan sesuatu yang lain dengan UB dari yang Anda inginkan.
Wildcard

@ MM Maaf, tidak ada akses array selain yang sepele melalui penunjuk ke objek itu sendiri. Dan yang kedua hanyalah contoh efek samping yang mudah dilihat dari perilaku tidak terdefinisi; kompilator mengoptimalkan pembacaan menjadi xkarena ia tahu Anda tidak dapat mengubahnya dengan cara yang ditentukan. Pengoptimalan serupa dapat terjadi ketika Anda mengubah bmelalui (&blah.a)[1]jika kompilator dapat membuktikan tidak ada akses yang ditentukan byang 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.
Yakk - Adam Nevraumont

6

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 Datastruct dalam kode ini juga termasuk overloads untuk operator subscript, untuk elemen akses diindeks dalam yang aranggota array, serta begindanend 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;), sebuah Proxyobjek dikembalikan. Kemudian, karena nilai Proxyr ini bukan penunjuk, ->operatornya sendiri disebut rantai otomatis, yang mengembalikan penunjuk ke dirinya sendiri. Dengan cara ini, Proxyobjek dibuat instance-nya dan tetap valid selama evaluasi ekspresi awal.

Konstruksi Proxyobjek mengisi 3 anggota referensinya a, bdan cmenurut pointer yang diteruskan dalam konstruktor, yang diasumsikan mengarah ke buffer yang berisi setidaknya 3 nilai yang tipenya diberikan sebagai parameter template T. Jadi alih-alih menggunakan referensi bernama yang merupakan anggota Datakelas, 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 versi main(). The #if 1Versi menggunakan ->dan []operator, dan #if 0Melakukan 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 1dan #if 0sebelum yang pertama main()untuk membandingkan): https://godbolt.org/g/QqRWZb

#include <iostream>
#include <ctime>

template <typename T>
class Proxy {
public:
    T &a, &b, &c;
    Proxy(T* par) : a(par[0]), b(par[1]), c(par[2]) {}
    Proxy* operator -> () { return this; }
};

struct Data {
    int ar[3];
    template <typename I> int& operator [] (I idx) { return ar[idx]; }
    template <typename I> const int& operator [] (I idx) const { return ar[idx]; }
    Proxy<int>       operator -> ()       { return Proxy<int>(ar); }
    Proxy<const int> operator -> () const { return Proxy<const int>(ar); }
    int* begin()             { return ar; }
    const int* begin() const { return ar; }
    int* end()             { return ar + sizeof(ar)/sizeof(int); }
    const int* end() const { return ar + sizeof(ar)/sizeof(int); }
};

// Nci returns an unpredictible int
inline int Nci() {
    static auto t = std::time(nullptr) / 100 * 100;
    return static_cast<int>(t++ % 1000);
}

#if 1
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d->b << "\n";
    d->b = -5;
    std::cout << d[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd->c << "\n";
    //cd->c = -5;  // error: assignment of read-only location
    std::cout << cd[2] << "\n";
}
#else
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d.ar[1] << "\n";
    d->b = -5;
    std::cout << d.ar[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd.ar[2] << "\n";
    //cd.ar[2] = -5;
    std::cout << cd.ar[2] << "\n";
}
#endif

Bagus. Suara positif terutama karena Anda membuktikan bahwa ini mengoptimalkan. BTW, Anda dapat melakukannya dengan lebih mudah dengan menulis fungsi yang sangat sederhana, bukan keseluruhan main()dengan fungsi pengaturan waktu! misalnya int getb(Data *d) { return (*d)->b; }mengkompilasi ke hanya mov eax, DWORD PTR [rdi+4]/ ret( godbolt.org/g/89d3Np ). (Ya, Data &dakan membuat sintaksnya lebih mudah, tetapi saya menggunakan penunjuk alih-alih ref untuk menyoroti keanehan kelebihan beban ->dengan cara ini.)
Peter Cordes

Bagaimanapun, ini keren. Ide lain seperti int tmp[] = { a, b, c}; return tmp[idx];tidak dioptimalkan, jadi rapi yang ini bisa.
Peter Cordes

Satu lagi alasan saya ketinggalan operator.di C ++ 17.
Jens

2

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:

char index_data(const struct data *d, size_t index) {
  assert(sizeof(*d) == offsetoff(*d, c)+1);
  assert(index < sizeof(*d));
  char buf[sizeof(*d)];
  memcpy(buf, d, sizeof(*d));
  return buf[index];
}

Untuk versi C ++ saja, Anda mungkin ingin menggunakan static_assertuntuk memverifikasi bahwa struct datamemiliki tata letak standar, dan mungkin melemparkan pengecualian pada indeks yang tidak valid.


1

Ini ilegal, tetapi ada solusi lain:

struct data {
    union {
        struct {
            int a;
            int b;
            int c;
        };
        int v[3];
    };
};

Sekarang Anda dapat mengindeks v:


6
Banyak proyek c ++ menganggap downcasting di semua tempat tidak masalah. Kita tetap tidak boleh memberitakan praktik buruk.
StoryTeller - Unslander Monica

2
Serikat pekerja memecahkan masalah aliasing yang ketat dalam kedua bahasa. Tetapi jenis permainan kata melalui serikat pekerja hanya baik-baik saja di C, tidak di C ++.
Lundin

1
tetap saja, saya tidak akan terkejut jika ini berfungsi pada 100% dari semua kompiler c ++. pernah.
Sven Nilsson

1
Anda dapat mencobanya di gcc dengan pengaturan pengoptimal paling agresif aktif.
Lundin

1
@Lundin: punning tipe union legal di GNU C ++, sebagai ekstensi dari ISO C ++. Sepertinya tidak disebutkan dengan sangat jelas di manual , tapi saya cukup yakin tentang ini. Namun, jawaban ini perlu menjelaskan di mana itu valid dan di mana tidak.
Peter Cordes
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.