คืออะไร ":-!!" ในรหัส C?


1665

ฉันชนรหัสมาโครแปลก ๆ นี้ใน/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); }))

อะไร:-!!ทำอย่างไร


2
- เอกนารีลบ <br />! ตรรกะไม่ใช่ <br /> ผกผันไม่ใช่จำนวนเต็มที่กำหนดดังนั้นตัวแปรอาจเป็น 0 หรือ 1
CyrillC

69
คอมไพล์ตำหนิบอกเราว่าแบบฟอร์มนี้โดยเฉพาะอย่างยิ่งของการยืนยันแบบคงที่ได้รับการแนะนำให้รู้จักกับแจ Beulich ใน 8c87df4 เห็นได้ชัดว่าเขามีเหตุผลที่ดีที่จะทำมัน (ดูข้อความยืนยัน)
Niklas B.

55
@Lundin: ยืนยัน () ไม่ทำให้เกิดข้อผิดพลาดในการคอมไพล์ นั่นคือจุดรวมของการก่อสร้างข้างต้น
Chris Pacejo

4
@GreweKokkor อย่าไร้เดียงสา Linux มีขนาดใหญ่เกินไปที่คนคนหนึ่งจะจัดการได้ทั้งหมด ไลนัสมีอาวุธของเขาและพวกเขามีแรงผลักดันการเปลี่ยนแปลงและการพัฒนาจากล่างขึ้นบน ไลนัสตัดสินใจว่าเขาต้องการคุณสมบัติหรือไม่ แต่เขาเชื่อใจเพื่อนร่วมงานในระดับหนึ่ง หากคุณต้องการทราบข้อมูลเพิ่มเติมเกี่ยวกับการทำงานของระบบกระจายในสภาพแวดล้อมโอเพ่นซอร์สตรวจสอบวิดีโอ youtube: youtube.com/watch?v=4XpnKHJAok8 (มันเป็นการพูดคุยที่น่าสนใจมาก)
Tomas Pruzina

3
@cpcloud sizeofไม่ "ประเมิน" ประเภทไม่ใช่ค่า เป็นประเภทที่ไม่ถูกต้องในกรณีนี้
Winston Ewert

คำตอบ:


1692

นี้จะมีผลวิธีการตรวจสอบว่าอีแสดงออกสามารถประเมินเป็น 0 และหากไม่ได้ที่จะล้มเหลวสร้าง

มาโครค่อนข้างผิด มันควรจะเป็นบางสิ่งบางอย่างมากขึ้นเช่นมากกว่าBUILD_BUG_OR_ZERO ...ON_ZERO(มีการพูดคุยกันเป็นครั้งคราวว่าเป็นชื่อที่สับสนหรือไม่)

คุณควรอ่านนิพจน์เช่นนี้:

sizeof(struct { int: -!!(e); }))
  1. (e): eนิพจน์

  2. !!(e): มีเหตุผลคัดค้านสองครั้ง: 0ถ้าe == 0; 1มิฉะนั้น

  3. -!!(e): ลบล้างนิพจน์จากขั้นตอนที่ 2: 0ถ้าเป็น0เช่นนั้น -1มิฉะนั้น

  4. struct{int: -!!(0);} --> struct{int: 0;}: ถ้ามันเป็นศูนย์เราจะประกาศ struct ด้วย bitfield จำนวนเต็มแบบไม่ระบุชื่อที่มีความกว้างเป็นศูนย์ ทุกอย่างเรียบร้อยและเราดำเนินการตามปกติ

  5. struct{int: -!!(1);} --> struct{int: -1;}: ในทางกลับกันถ้ามันไม่เป็นศูนย์แล้วมันจะเป็นจำนวนลบ การประกาศบิตฟิลด์ใด ๆ ที่มีความกว้างติดลบเป็นข้อผิดพลาดในการรวบรวม

ดังนั้นเราจะจบด้วยบิตฟิลด์ที่มีความกว้าง 0 ในโครงสร้างซึ่งดีหรือบิตฟิลด์ที่มีความกว้างติดลบซึ่งเป็นข้อผิดพลาดในการรวบรวม จากนั้นเราก็ใช้sizeofฟิลด์นั้นดังนั้นเราจึงได้size_tความกว้างที่เหมาะสม (ซึ่งจะเป็นศูนย์ในกรณีที่eเป็นศูนย์)


บางคนได้ถาม: ทำไมไม่เพียงใช้assert?

คำตอบของ keithmoที่นี่มีการตอบรับที่ดี:

มาโครเหล่านี้ใช้การทดสอบแบบคอมไพล์ขณะที่ assert () เป็นการทดสอบแบบรันไทม์

ถูกต้อง คุณไม่ต้องการตรวจสอบปัญหาในเคอร์เนลของคุณตอนรันไทม์ที่อาจติดอยู่ก่อนหน้านี้! มันเป็นส่วนสำคัญของระบบปฏิบัติการ ทุกปัญหาสามารถตรวจจับได้ในเวลารวบรวม


5
@weston มีสถานที่ต่าง ๆ มากมาย ดูด้วยตัวคุณเอง!
John Feminella

166
ตัวแปรล่าสุดของ C ++ หรือมาตรฐาน C มีลักษณะคล้ายstatic_assertกับวัตถุประสงค์ที่เกี่ยวข้อง
Basile Starynkevitch

54
@Lundin - #error จะต้องใช้รหัส 3 บรรทัด # ถ้า / # ข้อผิดพลาด / # endif และจะทำงานเฉพาะสำหรับการประเมินผลที่สามารถเข้าถึงโปรเซสเซอร์ล่วงหน้าได้ แฮ็คนี้ใช้สำหรับการประเมินใด ๆ ที่คอมไพเลอร์เข้าถึงได้
Ed Staub

236
เคอร์เนล Linux ไม่ได้ใช้ C ++ อย่างน้อยก็ในขณะที่ Linus ยังมีชีวิตอยู่
Mark Ransom

6
@ Dolda2000: " นิพจน์บูลีนใน C ถูกกำหนดให้ประเมินค่าเป็นศูนย์หรือหนึ่งเสมอ " - ไม่แน่นอน ผู้ประกอบการที่อัตราผลตอบแทนที่ "บูลมีเหตุผล" ผล ( !, <, >, <=, >=, ==, !=, &&, ||) เสมอผลผลิต 0 หรือ 1 การแสดงออกอื่น ๆ อาจให้ผลลัพธ์ที่อาจนำมาใช้เป็นเงื่อนไข แต่เป็นเพียงศูนย์หรือไม่ใช่ศูนย์; ตัวอย่างเช่นisdigit(c)ซึ่งcเป็นหลักสามารถให้ผลใด ๆ ที่ไม่ใช่ศูนย์ค่า (ซึ่งจะถือว่าเป็นจริงในเงื่อนไข)
Keith Thompson

256

:เป็น bitfield สำหรับ!!นั่นคือการปฏิเสธคู่ตรรกะและดังนั้นผลตอบแทน0สำหรับเท็จหรือ1จริง และ-เครื่องหมายลบคือลบล้างเลขคณิต

มันเป็นเพียงกลอุบายที่จะทำให้คอมไพเลอร์มีปัญหาเรื่องอินพุตที่ไม่ถูกต้อง

BUILD_BUG_ON_ZEROพิจารณา เมื่อ-!!(e)ประเมินเป็นค่าลบที่ก่อให้เกิดข้อผิดพลาดในการรวบรวม มิฉะนั้น-!!(e)ประเมินเป็น 0 และบิตฟิลด์ความกว้าง 0 มีขนาดเป็น 0 และดังนั้นแมโครประเมินเป็น a size_tด้วยค่า 0

ชื่ออ่อนแอในมุมมองของฉันเพราะการสร้างในความเป็นจริงล้มเหลวเมื่ออินพุตไม่เป็นศูนย์

BUILD_BUG_ON_NULL คล้ายกันมาก แต่ให้ผลผลิตตัวชี้มากกว่า intแต่ผลตอบแทนถัวเฉลี่ยตัวชี้มากกว่า


14
เป็นsizeof(struct { int:0; })ไปตามกลไกการอย่างเคร่งครัด?
ouah

7
ทำไมผลลัพธ์โดยทั่วไปจะเป็น0อย่างไร A structมีเพียง bitfield ที่ว่างเปล่าจริง แต่ฉันไม่คิดว่าอนุญาตให้ใช้ struct ที่มีขนาด 0 เช่นถ้าคุณสร้างอาเรย์ประเภทนั้นองค์ประกอบอาเรย์แต่ละรายการจะต้องมีที่อยู่ต่างกันใช่ไหม
Jens Gustedt

2
ที่จริงแล้วพวกเขาไม่สนใจเนื่องจากใช้ส่วนขยาย GNU พวกเขาปิดใช้งานกฎนามแฝงที่เข้มงวดและไม่พิจารณาจำนวนเต็มล้นเป็น UB แต่ฉันสงสัยว่าสิ่งนี้สอดคล้องกับ C.
ouah

3
@ouah เกี่ยวกับ bitfields ความยาวศูนย์ที่ไม่มีชื่อโปรดดูที่นี่: stackoverflow.com/questions/4297095/…
David Heffernan

9
@DavidHeffernan จริง ๆ แล้วอนุญาตให้บิตฟิลด์ที่0ไม่มีชื่อของความกว้าง แต่ไม่ใช่ถ้าไม่มีสมาชิกที่มีชื่ออื่นในโครงสร้าง (C99, 6.7.2.1p2) "If the struct-declaration-list contains no named members, the behavior is undefined."ตัวอย่างเช่นsizeof (struct {int a:1; int:0;})มีการปฏิบัติตามอย่างเคร่งครัด แต่sizeof(struct { int:0; })ไม่ใช่ (พฤติกรรมที่ไม่ได้กำหนด)
ouah

168

บางคนดูเหมือนจะสับสนกับมาโครเหล่านี้ assert()บางคนดูเหมือนจะสับสนแมโครเหล่านี้ด้วย

มาโครเหล่านี้ใช้การทดสอบแบบคอมไพล์ขณะที่assert()เป็นการทดสอบรันไทม์


52

ฉันค่อนข้างประหลาดใจที่ไม่มีการกล่าวถึงทางเลือกในไวยากรณ์นี้ กลไกทั่วไปอื่น (แต่เก่ากว่า) คือการเรียกใช้ฟังก์ชันที่ไม่ได้กำหนดไว้และใช้เครื่องมือเพิ่มประสิทธิภาพเพื่อรวบรวมการเรียกใช้ฟังก์ชันหากการยืนยันของคุณถูกต้อง

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

ในขณะที่กลไกนี้ทำงาน (ตราบเท่าที่เปิดใช้งานการปรับให้เหมาะสมที่สุด) ก็มีข้อเสียของการไม่รายงานข้อผิดพลาดจนกว่าคุณจะลิงก์ซึ่งในเวลานั้นมันล้มเหลวในการค้นหาคำจำกัดความสำหรับฟังก์ชั่น you_did_something_bad () นั่นเป็นสาเหตุที่นักพัฒนาเคอร์เนลเริ่มใช้ลูกเล่นเช่นความกว้างบิตฟิลด์ขนาดลบและอาร์เรย์ขนาดลบ (ภายหลังซึ่งหยุดการสร้างบิลด์ใน GCC 4.4)

ในความเห็นอกเห็นใจต่อความต้องการการยืนยันเวลารวบรวม GCC 4.3 ได้เปิดตัวerrorฟังก์ชั่นฟังก์ชั่นที่ช่วยให้คุณสามารถขยายความคิดเก่า ๆ นี้ได้ แต่สร้างข้อผิดพลาดในการคอมไพล์เวลาด้วยข้อความที่คุณเลือก "ข้อความผิดพลาด!

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

ในความเป็นจริงในฐานะของ Linux 3.9 ตอนนี้เรามีมาโครที่เรียกcompiletime_assertใช้ซึ่งใช้คุณสมบัตินี้และมาโครส่วนใหญ่bug.hได้รับการอัพเดตตามนั้น อย่างไรก็ตามแมโครนี้ไม่สามารถใช้เป็นเครื่องมือเริ่มต้นได้ อย่างไรก็ตามการใช้คำสั่งแสดงออก (ส่วนขยาย GCC อื่น) คุณสามารถทำได้!

#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;                                                    \
    })

มาโครนี้จะประเมินพารามิเตอร์ของมันหนึ่งครั้ง (ในกรณีที่มีผลข้างเคียง) และสร้างข้อผิดพลาดในการคอมไพล์เวลาที่ระบุว่า "ฉันบอกคุณว่าอย่าให้ฉันห้าอัน!" ถ้านิพจน์ประเมินถึงห้าหรือไม่ใช่ค่าคงที่เวลาคอมไพล์

เหตุใดเราจึงไม่ใช้สิ่งนี้แทนที่จะเป็นบิตบิตขนาดลบ อนิจจาในปัจจุบันมีข้อ จำกัด มากมายในการใช้คำสั่งนิพจน์รวมถึงการใช้เป็น initializers คงที่ (สำหรับค่าคงที่ enum ความกว้างบิตฟิลด์และอื่น ๆ ) แม้ว่านิพจน์คำสั่งจะคงที่อย่างสมบูรณ์ด้วยตนเอง (เช่นสามารถประเมินได้อย่างเต็มที่ ที่รวบรวมเวลาและอื่น ๆ ผ่าน__builtin_constant_p()ทดสอบ) นอกจากนี้พวกเขาไม่สามารถใช้นอกร่างกายฟังก์ชั่น

หวังว่า GCC จะแก้ไขข้อบกพร่องเหล่านี้ในไม่ช้าและอนุญาตให้มีการใช้คำสั่งคงที่เป็นนิพจน์เริ่มต้นอย่างต่อเนื่อง ความท้าทายที่นี่คือข้อกำหนดภาษาที่กำหนดว่านิพจน์คงที่ทางกฎหมายคืออะไร C ++ 11 เพิ่มคำหลัก constexpr สำหรับประเภทหรือสิ่งนี้ แต่ไม่มีคู่ใน C11 ในขณะที่ C11 ได้รับการยืนยันแบบคงที่ซึ่งจะแก้ปัญหาบางส่วนของปัญหานี้จะไม่แก้ข้อบกพร่องเหล่านี้ทั้งหมด ดังนั้นฉันหวังว่า gcc สามารถทำให้การทำงานของ constexpr พร้อมใช้งานเป็นส่วนขยายผ่าน -std = gnuc99 & -std = gnuc11 หรือบางอย่างและอนุญาตให้ใช้กับนิพจน์คำสั่งและ อัล


6
โซลูชันทั้งหมดของคุณไม่ใช่ทางเลือก ความคิดเห็นข้างบนมาโครนั้นค่อนข้างชัดเจน " so the expression can be used e.g. in a structure initializer (or where-ever else comma expressions aren't permitted)." มาโครส่งคืนนิพจน์ประเภทsize_t
Wiz

3
@Wiz ใช่ฉันรู้เรื่องนี้แล้ว บางทีนี่อาจเป็น verbose เล็กน้อยและบางทีฉันอาจต้องการเยี่ยมชมถ้อยคำของฉันอีกครั้ง แต่ประเด็นของฉันคือการสำรวจกลไกต่าง ๆ สำหรับการยืนยันแบบคงที่และแสดงให้เห็นว่าทำไมเรายังคงใช้บิตฟิลด์ขนาดลบ ในระยะสั้นถ้าเราได้รับกลไกสำหรับการแสดงออกคำสั่งคงที่เราจะเปิดตัวเลือกอื่น ๆ
Daniel Santos

อย่างไรก็ตามเราไม่สามารถใช้แมโครเหล่านี้สำหรับตัวแปรได้ ขวา? error: bit-field ‘<anonymous>’ width not an integer constantมันช่วยให้ค่าคงที่เท่านั้น ดังนั้นการใช้งานคืออะไร?
Karthik Raj Palanichamy

1
@Karthik ค้นหาที่มาของเคอร์เนล Linux เพื่อดูว่าทำไมจึงใช้งาน
Daniel Santos

@supercat ฉันไม่เห็นว่าความคิดเห็นของคุณเกี่ยวข้องกันอย่างไร คุณกรุณาแก้ไขได้ดีกว่าอธิบายสิ่งที่คุณหมายถึงหรือลบออกได้ดีกว่า
Daniel Santos

36

มันสร้าง0bitfield ขนาดถ้าเงื่อนไขเป็นเท็จ แต่bitfield ขนาด-1( -!!1) ถ้าเงื่อนไขเป็นจริง / ไม่ใช่ศูนย์ ในกรณีก่อนหน้านี้ไม่มีข้อผิดพลาดและโครงสร้างจะเริ่มต้นด้วยสมาชิก int ในกรณีหลังมีข้อผิดพลาดในการคอมไพล์ (และไม่มีสิ่งใดเช่น-1บิตฟิลด์ขนาดถูกสร้างขึ้นแน่นอน)


3
ที่จริงแล้วมันจะคืนsize_tค่าa ด้วยค่า 0 ในกรณีที่เงื่อนไขเป็นจริง
David Heffernan
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.