Những gì là ":-!!" trong mã C?


1665

Tôi đã va vào mã macro kỳ lạ này trong /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); }))

Không gì :-!!làm gì?


2
- Trừ điểm đơn phương <br />! Logic KHÔNG <br /> nghịch đảo không phải của Số nguyên e đã cho nên biến có thể là 0 hoặc 1.
CyrillC

69
git đổ lỗi cho chúng tôi biết rằng hình thức khẳng định tĩnh đặc biệt này đã được Jan Beulich giới thiệu vào năm 8c87df4 . Rõ ràng anh ta có lý do chính đáng để làm điều đó (xem thông điệp cam kết).
Niklas B.

55
@Lundin: assert () KHÔNG gây ra lỗi thời gian biên dịch. Đó là toàn bộ quan điểm của việc xây dựng ở trên.
Chris Pacejo

4
@GreweKokkor Đừng ngây thơ, Linux quá lớn đối với một người để xử lý tất cả. Linus có các trung úy của mình và họ có những người thúc đẩy những thay đổi và cải tiến từ dưới lên. Linus chỉ quyết định liệu anh ta có muốn tính năng hay không, nhưng anh ta tin tưởng đồng nghiệp ở một mức độ nào đó. Nếu bạn muốn biết thêm về cách hệ thống phân tán hoạt động trong môi trường nguồn mở, hãy xem video youtube: youtube.com/watch?v=4XpnKHJAok8 (Đây là cuộc nói chuyện rất thú vị).
Tomas Pruzina

3
@cpcloud, sizeofkhông "đánh giá" loại, chỉ là không phải giá trị. Đây là loại không hợp lệ trong trường hợp này.
Winston Ewert

Câu trả lời:


1692

Trên thực tế, đây là một cách để kiểm tra xem biểu thức e có thể được đánh giá là 0 hay không, và nếu không, sẽ thất bại trong quá trình xây dựng .

Macro có phần bị đặt tên sai; nó nên là một cái gì đó giống như BUILD_BUG_OR_ZERO, hơn là ...ON_ZERO. ( Thỉnh thoảng có những cuộc thảo luận về việc liệu đây có phải là một cái tên khó hiểu hay không .)

Bạn nên đọc biểu thức như thế này:

sizeof(struct { int: -!!(e); }))
  1. (e): Tính biểu thức e.

  2. !!(e): Phủ định logic hai lần: 0if e == 0; bằng cách khác 1.

  3. -!!(e): Phủ định số từ biểu thức từ bước 2: 0nếu có 0; bằng cách khác -1.

  4. struct{int: -!!(0);} --> struct{int: 0;}: Nếu nó bằng 0, thì chúng tôi khai báo một cấu trúc với một bitfield số nguyên ẩn danh có chiều rộng bằng không. Mọi thứ đều ổn và chúng tôi tiến hành như bình thường.

  5. struct{int: -!!(1);} --> struct{int: -1;}: Mặt khác, nếu nó không bằng 0, thì đó sẽ là một số âm. Khai báo bất kỳ bitfield nào có chiều rộng âm là lỗi biên dịch.

Vì vậy, chúng ta sẽ kết thúc với một bitfield có chiều rộng 0 trong một cấu trúc, điều này tốt hoặc một bitfield có chiều rộng âm, đó là một lỗi biên dịch. Sau đó, chúng tôi lấy sizeoftrường đó, vì vậy chúng tôi nhận được a size_tvới chiều rộng phù hợp (sẽ bằng 0 trong trường hợp ebằng 0).


Một số người đã hỏi: Tại sao không chỉ sử dụng một assert?

Câu trả lời của keithmo ở đây có một phản hồi tốt:

Các macro này thực hiện kiểm tra thời gian biên dịch, trong khi khẳng định () là kiểm tra thời gian chạy.

Chính xác. Bạn không muốn phát hiện các vấn đề trong kernel của mình trong thời gian chạy có thể bị bắt trước đó! Đây là một phần quan trọng của hệ điều hành. Ở bất kỳ mức độ nào, các vấn đề có thể được phát hiện tại thời điểm biên dịch, càng nhiều thì càng tốt.


5
@weston Rất nhiều nơi khác nhau. Xem cho chính mình!
John Women'sella

166
các biến thể gần đây của tiêu chuẩn C ++ hoặc C có một cái gì đó giống như static_assertcho các mục đích liên quan.
Basile Starynkevitch

54
@Lundin - #error sẽ yêu cầu sử dụng 3 dòng mã # if / # error / # endif và sẽ chỉ hoạt động đối với các đánh giá có thể truy cập được của bộ xử lý trước. Hack này hoạt động cho bất kỳ đánh giá có thể truy cập vào trình biên dịch.
Ed Staub

236
Nhân Linux không sử dụng C ++, ít nhất là trong khi Linus vẫn còn sống.
Đánh dấu tiền chuộc

6
@ Dolda2000: " Biểu thức Boolean trong C được xác định để luôn luôn ước tính bằng 0 hoặc một " - Không chính xác. Các nhà khai thác mà năng suất kết quả "một cách logic boolean" ( !, <, >, <=, >=, ==,!= , &&, ||) luôn mang lại 0 hoặc 1. biểu thức khác có thể mang lại kết quả có thể được sử dụng như một điều kiện, nhưng chỉ đơn thuần là không hay khác không; ví dụ, isdigit(c)trong đó clà một chữ số, có thể mang lại bất kỳ giá trị khác không (sau đó được coi là đúng trong một điều kiện).
Keith Thompson

256

Đây :là một bitfield. Đối với !!, đó là phủ định kép logic và do đó trả về 0sai hoặc 1đúng. Và -là một dấu trừ, tức là phủ định số học.

Tất cả chỉ là một mẹo để đưa trình biên dịch vào barf trên các đầu vào không hợp lệ.

Hãy xem xét BUILD_BUG_ON_ZERO. Khi -!!(e)ước tính giá trị âm, điều đó tạo ra lỗi biên dịch. Mặt khác ước tính -!!(e)thành 0 và bitfield có độ rộng 0 có kích thước bằng 0. Và do đó, macro đánh giá thành a size_tvới giá trị 0.

Tên của tôi yếu theo quan điểm của tôi vì thực tế việc xây dựng thất bại khi đầu vào không bằng không.

BUILD_BUG_ON_NULLlà rất giống nhau, nhưng mang lại một con trỏ chứ không phải là một int.


14
sizeof(struct { int:0; })tuân thủ nghiêm ngặt?
ouah

7
Tại sao kết quả nói chung sẽ là 0? A structchỉ có một bitfield trống, đúng, nhưng tôi không nghĩ rằng cấu trúc có kích thước 0 được cho phép. Ví dụ: nếu bạn tạo một mảng kiểu đó, các thành phần mảng riêng lẻ vẫn phải có các địa chỉ khác nhau, phải không?
Jens Gustyt

2
họ thực sự không quan tâm vì họ sử dụng các phần mở rộng GNU, họ vô hiệu hóa quy tắc bí danh nghiêm ngặt và không coi số nguyên tràn là UB. Nhưng tôi đã tự hỏi nếu điều này là tuân thủ nghiêm ngặt C.
ouah

3
@ouah liên quan đến bitfield có độ dài bằng 0 không tên, xem tại đây: stackoverflow.com/questions/4297095/
David Heffernan

9
@DavidHeffernan thực sự C cho phép trường bit có 0chiều rộng không được đặt tên, nhưng không có nếu không có thành viên có tên nào khác trong cấu trúc. (C99, 6.7.2.1p2) "If the struct-declaration-list contains no named members, the behavior is undefined."Vì vậy, ví dụ sizeof (struct {int a:1; int:0;})là tuân thủ nghiêm ngặt nhưng sizeof(struct { int:0; })không (hành vi không xác định).
ouah

168

Một số người dường như nhầm lẫn các macro này với assert().

Các macro này thực hiện kiểm tra thời gian biên dịch, trong khi đó assert()là kiểm tra thời gian chạy.


52

Chà, tôi khá ngạc nhiên khi các lựa chọn thay thế cho cú pháp này chưa được đề cập. Một cơ chế phổ biến khác (nhưng cũ hơn) là gọi một hàm không được xác định và dựa vào trình tối ưu hóa để biên dịch lệnh gọi hàm nếu khẳng định của bạn là đúng.

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

Mặc dù cơ chế này hoạt động (miễn là bật tối ưu hóa), nó có nhược điểm là không báo cáo lỗi cho đến khi bạn liên kết, tại thời điểm đó, nó không tìm thấy định nghĩa cho hàm you_did_s Something_bad (). Đó là lý do tại sao các nhà phát triển nhân bắt đầu sử dụng các thủ thuật như độ rộng trường bit có kích thước âm và các mảng có kích thước âm (sau này đã ngừng phá vỡ các bản dựng trong GCC 4.4).

Để thông cảm cho nhu cầu xác nhận thời gian biên dịch, GCC 4.3 đã giới thiệu errorthuộc tính hàm cho phép bạn mở rộng khái niệm cũ hơn này, nhưng tạo ra lỗi thời gian biên dịch với thông báo bạn chọn - không còn là mảng có kích thước âm "khó hiểu" " thông báo lỗi!

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

Trên thực tế, kể từ Linux 3.9, giờ đây chúng ta có một macro được gọi là compiletime_assertsử dụng tính năng này và hầu hết các macro trong bug.hđã được cập nhật tương ứng. Tuy nhiên, macro này không thể được sử dụng như một trình khởi tạo. Tuy nhiên, bằng cách sử dụng bằng biểu thức câu lệnh (một phần mở rộng C GCC khác), bạn có thể!

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

Macro này sẽ đánh giá tham số của nó chính xác một lần (trong trường hợp nó có tác dụng phụ) và tạo ra lỗi thời gian biên dịch có nội dung "Tôi đã nói với bạn rằng đừng cho tôi năm!" nếu biểu thức ước lượng đến năm hoặc không phải là hằng số thời gian biên dịch.

Vậy tại sao chúng ta không sử dụng cái này thay vì các trường bit có kích thước âm? Than ôi, hiện tại có nhiều hạn chế trong việc sử dụng biểu thức câu lệnh, bao gồm cả việc sử dụng chúng làm bộ khởi tạo không đổi (đối với hằng số enum, độ rộng trường bit, v.v.) ngay cả khi biểu thức câu lệnh hoàn toàn không đổi (nghĩa là có thể được đánh giá đầy đủ tại thời gian biên dịch và mặt khác vượt qua__builtin_constant_p() bài kiểm tra). Hơn nữa, chúng không thể được sử dụng bên ngoài cơ thể chức năng.

Hy vọng, GCC sẽ sớm sửa đổi những thiếu sót này và cho phép các biểu thức câu lệnh không đổi được sử dụng làm công cụ khởi tạo liên tục. Thách thức ở đây là đặc tả ngôn ngữ xác định biểu thức hằng số pháp lý là gì. C ++ 11 đã thêm từ khóa constexpr cho loại hoặc điều này, nhưng không có đối tác tồn tại trong C11. Mặc dù C11 đã nhận được các xác nhận tĩnh, sẽ giải quyết một phần của vấn đề này, nhưng nó sẽ không giải quyết được tất cả những thiếu sót này. Vì vậy, tôi hy vọng rằng gcc có thể cung cấp chức năng constexpr dưới dạng tiện ích mở rộng thông qua -std = gnuc99 & -std = gnuc11 hoặc một số như vậy và cho phép sử dụng nó trên biểu thức câu lệnh et. al.


6
Tất cả các giải pháp của bạn KHÔNG phải là lựa chọn thay thế. Nhận xét phía trên macro khá rõ ràng " so the expression can be used e.g. in a structure initializer (or where-ever else comma expressions aren't permitted)." Macro trả về một biểu thức loạisize_t
Wiz

3
@Wiz Vâng, tôi biết điều này. Có lẽ đây là một chút dài dòng và có lẽ tôi cần truy cập lại từ ngữ của mình, nhưng quan điểm của tôi là khám phá các cơ chế khác nhau để xác nhận tĩnh và cho thấy lý do tại sao chúng ta vẫn sử dụng các bitfield có kích thước âm. Nói tóm lại, nếu chúng ta có một cơ chế cho biểu thức câu lệnh không đổi, chúng ta sẽ có các tùy chọn khác mở.
Daniel Santos

Dù sao, chúng ta không thể sử dụng các macro này cho một biến. đúng? error: bit-field ‘<anonymous>’ width not an integer constantNó chỉ cho phép hằng số. Vì vậy, những gì sử dụng?
Karthik Raj Palanichamy

1
@Karthik Tìm kiếm các nguồn của nhân Linux để xem tại sao nó được sử dụng.
Daniel Santos

@supercat Tôi không thấy bình luận của bạn thế nào cả. Bạn có thể vui lòng sửa lại nó, giải thích rõ hơn ý của bạn hoặc loại bỏ nó?
Daniel Santos

36

Nó tạo ra một 0bitfield kích thước nếu điều kiện là sai, nhưng một bitfield kích thước -1( -!!1) nếu điều kiện là đúng / khác không. Trong trường hợp trước, không có lỗi và struct được khởi tạo với một thành viên int. Trong trường hợp thứ hai, có một lỗi biên dịch (và -1dĩ nhiên không có trường hợp nào như bitfield kích thước được tạo ra).


3
Trên thực tế, nó trả về size_tgiá trị 0 trong trường hợp điều kiện là đúng.
David Heffernan
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.