Apa yang ":-!!" dalam kode C?

1665

Saya bertemu dengan kode makro aneh ini di /usr/include/linux/kernel.h :

/* Force a compilation error if condition is true, but also produce a
   result (of value 0 and type size_t), so the expression can be used
   e.g. in a structure initializer (or where-ever else comma expressions
   aren't permitted). */
#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))
#define BUILD_BUG_ON_NULL(e) ((void *)sizeof(struct { int:-!!(e); }))

Apa yang :-!!harus dilakukan

chmurli
sumber
2
- Unary minus <br />! Logis TIDAK <br /> terbalik bukan dari Integer yang diberikan sehingga variabel bisa 0 atau 1.
CyrillC
69
git menyalahkan memberitahu kita bahwa bentuk pernyataan statis khusus ini diperkenalkan oleh Jan Beulich dalam 8c87df4 . Jelas dia punya alasan kuat untuk melakukannya (lihat pesan komit).
Niklas B.
55
@Lundin: assert () TIDAK menyebabkan kesalahan waktu kompilasi. Itulah inti dari konstruksi di atas.
Chris Pacejo
4
@ GreweKokkor Jangan naif, Linux terlalu besar untuk satu orang untuk menangani semuanya. Linus memiliki liutenant-nya dan mereka memiliki mereka yang mendorong perubahan dan peningkatan dari bawah ke atas. Linus hanya memutuskan apakah dia ingin fitur atau tidak, tetapi dia mempercayai rekan kerja sampai batas tertentu. Jika Anda ingin tahu lebih banyak tentang bagaimana sistem terdistribusi bekerja di lingkungan open source, periksa video youtube: youtube.com/watch?v=4XpnKHJAok8 (Ini adalah pembicaraan yang sangat menarik).
Tomas Pruzina
3
@ cpcloud, sizeoftidak "mengevaluasi" jenisnya, hanya saja bukan nilainya. Itu jenis yang tidak valid dalam kasus ini.
Winston Ewert

Jawaban:

1692

Ini, pada dasarnya, adalah cara untuk memeriksa apakah ekspresi e dapat dievaluasi menjadi 0, dan jika tidak, gagal membangun .

Makro ini agak salah nama; itu harus sesuatu yang lebih seperti BUILD_BUG_OR_ZERO, daripada ...ON_ZERO. (Ada diskusi sesekali tentang apakah ini nama yang membingungkan .)

Anda harus membaca ungkapan seperti ini:

sizeof(struct { int: -!!(e); }))
  1. (e): Hitung ekspresi e.

  2. !!(e): Meniadakan secara logis dua kali: 0 jika e == 0; jika tidak 1.

  3. -!!(e): Secara numerik meniadakan ekspresi dari langkah 2: 0 jika itu 0; jika tidak -1.

  4. struct{int: -!!(0);} --> struct{int: 0;}: Jika nol, maka kami mendeklarasikan struct dengan bitfield integer anonim yang memiliki lebar nol. Semuanya baik-baik saja dan kami melanjutkan seperti biasa.

  5. struct{int: -!!(1);} --> struct{int: -1;}: Di sisi lain, jika bukan nol, maka itu akan menjadi angka negatif. Mendeklarasikan bitfield apa pun dengan lebar negatif adalah kesalahan kompilasi.

Jadi kita akan berakhir dengan bitfield yang memiliki lebar 0 dalam struct, yang baik-baik saja, atau bitfield dengan lebar negatif, yang merupakan kesalahan kompilasi. Lalu kita ambil sizeofbidang itu, jadi kita dapatkan asize_t dengan lebar yang sesuai (yang akan menjadi nol dalam kasus di mana enol).


Beberapa orang bertanya: Mengapa tidak menggunakan saja assert?

jawaban keithmo sini memiliki respons yang baik:

Makro ini menerapkan uji waktu kompilasi, sementara assert () adalah uji waktu berjalan.

Tepat sekali. Anda tidak ingin mendeteksi masalah di kernel Anda saat runtime yang bisa ditangkap lebih awal! Ini bagian penting dari sistem operasi. Sejauh mana masalah dapat dideteksi pada waktu kompilasi, jauh lebih baik.

John Feminella
sumber
5
@ Barat Banyak tempat yang berbeda. Lihat diri mu sendiri!
John Feminella
166
varian terbaru dari C ++ atau standar C memiliki sesuatu seperti static_assertuntuk tujuan terkait.
Basile Starynkevitch
54
@Lundin - #error akan membutuhkan penggunaan 3 baris kode # if / # error / # endif, dan hanya akan berfungsi untuk evaluasi yang dapat diakses oleh pra-prosesor. Retasan ini berfungsi untuk setiap evaluasi yang dapat diakses oleh kompiler.
Ed Staub
236
Kernel Linux tidak menggunakan C ++, setidaknya tidak saat Linus masih hidup.
Mark Ransom
6
@ Dolda2000: " Ekspresi Boolean dalam C didefinisikan untuk selalu mengevaluasi ke nol atau satu " - Tidak persis. The operator yang yield "logis boolean" hasil ( !, <, >, <=, >=, ==, !=, &&, ||) selalu menghasilkan 0 atau 1. ekspresi lain dapat menghasilkan hasil yang dapat digunakan sebagai kondisi, tetapi hanya nol atau non-nol; misalnya, isdigit(c)di mana cadalah digit, dapat menghasilkan setiap non-nol nilai (yang kemudian diperlakukan sebagai benar dalam kondisi).
Keith Thompson
256

Ini :adalah bitfield. Adapun !!, itu adalah negasi ganda logis dan mengembalikan 0untuk false atau 1true. Dan itu -adalah tanda minus, yaitu negasi aritmatika.

Itu semua hanya trik untuk membuat kompiler untuk muntah pada input yang tidak valid.

Pertimbangkan BUILD_BUG_ON_ZERO. Ketika -!!(e)mengevaluasi ke nilai negatif, itu menghasilkan kesalahan kompilasi. Kalau tidak -!!(e)mengevaluasi ke 0, dan bitfield lebar 0 memiliki ukuran 0. Dan karenanya makro mengevaluasi ke size_tdengan nilai 0.

Nama saya lemah dalam pandangan saya karena build sebenarnya gagal ketika inputnya tidak nol.

BUILD_BUG_ON_NULLsangat mirip, tetapi menghasilkan pointer daripada int.

David Heffernan
sumber
14
secara sizeof(struct { int:0; })ketat sesuai?
ouah
7
Mengapa hasilnya secara umum 0? A structdengan hanya bitfield kosong, benar, tapi saya tidak berpikir bahwa struct dengan ukuran 0 diperbolehkan. Misalnya, jika Anda membuat array jenis itu, elemen array individual masih harus memiliki alamat yang berbeda, bukan?
Jens Gustedt
2
mereka sebenarnya tidak peduli karena mereka menggunakan ekstensi GNU, mereka menonaktifkan aturan aliasing yang ketat dan tidak menganggap integer overflow sebagai UB. Tapi saya bertanya-tanya apakah ini benar-benar sesuai C.
ouah
3
@ouah tentang bitfield tanpa panjang nol tanpa nama, lihat di sini: stackoverflow.com/questions/4297095/…
David Heffernan
9
@ Davidvidern sebenarnya C memungkinkan bit-field unamed 0lebar, tetapi tidak jika tidak ada anggota bernama lain dalam struktur. (C99, 6.7.2.1p2) "If the struct-declaration-list contains no named members, the behavior is undefined."Jadi misalnya sizeof (struct {int a:1; int:0;})sangat ketat tetapi sizeof(struct { int:0; })tidak (perilaku tidak terdefinisi).
ouah
168

Beberapa orang tampaknya membingungkan makro ini assert().

Makro ini menerapkan uji waktu kompilasi, sedangkan assert()uji runtime.

keithmo
sumber
52

Yah, saya cukup terkejut bahwa alternatif untuk sintaks ini belum disebutkan. Mekanisme umum lainnya (tetapi lebih tua) adalah untuk memanggil fungsi yang tidak didefinisikan dan mengandalkan pengoptimal untuk mengkompilasi-keluar panggilan fungsi jika pernyataan Anda benar.

#define MY_COMPILETIME_ASSERT(test)              \
    do {                                         \
        extern void you_did_something_bad(void); \
        if (!(test))                             \
            you_did_something_bad(void);         \
    } while (0)

Meskipun mekanisme ini berfungsi (selama optimisasi diaktifkan) ia memiliki kelemahan yaitu tidak melaporkan kesalahan hingga Anda menautkan, pada saat itu gagal menemukan definisi untuk fungsi you_did_something_bad (). Itu sebabnya pengembang kernel mulai menggunakan trik-trik seperti lebar bit-field berukuran negatif dan array berukuran negatif (yang kemudian berhenti memecah build di GCC 4.4).

Dalam simpati terhadap kebutuhan akan pernyataan waktu kompilasi, GCC 4.3 memperkenalkan erroratribut fungsi yang memungkinkan Anda untuk memperluas konsep yang lebih lama ini, tetapi menghasilkan kesalahan waktu kompilasi dengan pesan pilihan Anda - tidak ada lagi cryptic "array berukuran negatif "pesan kesalahan!

#define MAKE_SURE_THIS_IS_FIVE(number)                          \
    do {                                                        \
        extern void this_isnt_five(void) __attribute__((error(  \
                "I asked for five and you gave me " #number))); \
        if ((number) != 5)                                      \
            this_isnt_five();                                   \
    } while (0)

Bahkan, pada Linux 3.9, kami sekarang memiliki makro yang disebut compiletime_assertyang menggunakan fitur ini dan sebagian besar makro bug.htelah diperbarui. Namun, makro ini tidak dapat digunakan sebagai penginisialisasi. Namun, menggunakan ekspresi pernyataan (ekstensi-GCC lain), Anda bisa!

#define ANY_NUMBER_BUT_FIVE(number)                           \
    ({                                                        \
        typeof(number) n = (number);                          \
        extern void this_number_is_five(void) __attribute__(( \
                error("I told you not to give me a five!"))); \
        if (n == 5)                                           \
            this_number_is_five();                            \
        n;                                                    \
    })

Makro ini akan mengevaluasi parameternya tepat sekali (kalau-kalau ada efek samping) dan membuat kesalahan waktu kompilasi yang mengatakan, "Saya bilang jangan memberi saya lima!" jika ekspresi bernilai lima atau bukan konstanta waktu kompilasi.

Jadi mengapa kita tidak menggunakan ini alih-alih bidang bit berukuran negatif? Sayangnya, saat ini ada banyak pembatasan penggunaan ekspresi pernyataan, termasuk penggunaannya sebagai inisialisasi konstan (untuk konstanta enum, lebar bidang bit, dll.) Bahkan jika ekspresi pernyataan benar-benar konstan sendiri (yaitu, dapat sepenuhnya dievaluasi pada waktu kompilasi dan dinyatakan lulus __builtin_constant_p()tes). Lebih lanjut, mereka tidak dapat digunakan di luar fungsi tubuh.

Semoga, GCC akan segera mengubah kekurangan ini dan memungkinkan ekspresi pernyataan konstan untuk digunakan sebagai inisialisasi konstan. Tantangannya di sini adalah spesifikasi bahasa yang mendefinisikan apa yang merupakan ekspresi konstanta hukum. C ++ 11 menambahkan kata kunci constexpr untuk tipe atau hal ini saja, tetapi tidak ada padanan dalam C11. Sementara C11 memang mendapatkan pernyataan statis, yang akan menyelesaikan bagian dari masalah ini, C11 tidak akan menyelesaikan semua kekurangan ini. Jadi saya berharap bahwa gcc dapat membuat fungsionalitas constexpr tersedia sebagai ekstensi melalui -std = gnuc99 & -std = gnuc11 atau semacamnya dan memungkinkan penggunaannya pada pernyataan pernyataan et. Al.

Daniel Santos
sumber
6
Semua solusi Anda BUKAN alternatif. Komentar di atas makro cukup jelas " so the expression can be used e.g. in a structure initializer (or where-ever else comma expressions aren't permitted)." Makro mengembalikan ekspresi tipesize_t
Wiz
3
@ Ya Ya, saya tahu ini. Mungkin ini sedikit bertele-tele dan mungkin saya perlu mengunjungi kembali kata-kata saya, tetapi poin saya adalah untuk mengeksplorasi berbagai mekanisme untuk pernyataan statis dan menunjukkan mengapa kita masih menggunakan bitfield berukuran negatif. Singkatnya, jika kita mendapatkan mekanisme untuk ekspresi pernyataan konstan, kita akan memiliki opsi lain terbuka.
Daniel Santos
Lagi pula kita tidak bisa menggunakan makro ini untuk variabel. Baik? error: bit-field ‘<anonymous>’ width not an integer constantYang memungkinkan hanya konstanta. Jadi, apa gunanya?
Karthik Raj Palanichamy
1
@Karthik Cari sumber-sumber kernel Linux untuk melihat mengapa itu digunakan.
Daniel Santos
@supercat Saya tidak melihat bagaimana komentar Anda sama sekali terkait. Bisakah Anda merevisinya, lebih baik menjelaskan apa yang Anda maksud atau menghapusnya?
Daniel Santos
36

Ini menciptakan 0bitfield ukuran jika kondisinya salah, tetapi bitfield size -1( -!!1) jika kondisinya benar / tidak nol. Dalam kasus sebelumnya, tidak ada kesalahan dan struct diinisialisasi dengan anggota int. Dalam kasus terakhir, ada kesalahan kompilasi (dan tentu saja tidak ada -1bitfield ukuran yang dibuat).

Matt Phillips
sumber
3
Sebenarnya itu mengembalikan size_tdengan nilai 0 jika kondisinya benar.
David Heffernan