Câu hỏi "C - !!" trong mã C là gì?


Tôi đã gặp phải mã macro lạ này /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); }))

Cái gì :-!! làm gì?


1480
2018-02-10 14:50


gốc


@ Lundin: git đổ lỗi cho chúng ta biết rằng hình thức xác nhận tĩnh đặc biệt này là được giới thiệu bởi Jan Beulich trong 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.
@ Lundin: Nếu điều này là lý do để tránh xa các máy Linux, sau đó hãy đặt tên chỉ là một hệ điều hành hiện đại, mã nguồn trông đẹp hơn. - Sven Marnach
@SvenMarnach BSD hạt nhân bằng cách (: P). Không phải là tôi nghĩ rằng hạt nhân BSD là tốt hơn, nhưng họ thường hy sinh hiệu suất (và hackery) vì lợi ích của mã sạch. Ồ, vâng, tôi đã làm rối tung cả mã BSD và Linux. - AoeAoe
@ Lundin: Liệu BSOD dường như ít obfuscated cho bạn? Và bất cứ ai cũng có thể đóng góp cho Linux, vậy tại sao không chỉ cho một shot hơn là tránh xa một hệ điều hành hiện đại? - yati sagade
@ Lundin: assert () KHÔNG gây ra lỗi biên dịch. Đó là toàn bộ quan điểm của công trình trên. - Chris Pacejo


Các câu trả lời:


Đây là, có hiệu lực, 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, để không xây dựng được.

Macro có phần bị đặt tên sai; nó phải là một cái gì đó giống như BUILD_BUG_OR_ZERO, thay vì ...ON_ZERO. (Đã có đôi khi thảo luận về việc đây có phải là một cái tên khó hiểu.)

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

sizeof(struct { int: -!!(e); }))
  1. (e): Biểu thức tính toán e.

  2. !!(e): Phủ nhận hợp lý hai lần: 0 nếu e == 0; nếu không thì 1.

  3. -!!(e): Bỏ qua số biểu thức từ bước 2: 0 nếu nó là 0; nếu không thì -1.

  4. struct{int: -!!(0);} --> struct{int: 0;}: Nếu nó bằng không, thì chúng ta khai báo một cấu trúc với một bitfield số nguyên vô danh có độ rộng bằng 0. 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 phải không, thì nó sẽ là một số âm. Khai báo bất kỳ bitfield nào với tiêu cực chiều rộng là một lỗi biên dịch.

Vì vậy, chúng tôi sẽ hoặc là gió lên với một bitfield có chiều rộng 0 trong một cấu trúc, đó là tốt, hoặc một bitfield với chiều rộng tiêu cực, đó là một lỗi biên dịch. Sau đó chúng tôi lấy sizeof lĩnh vực đó, vì vậy chúng tôi có được size_t với chiều rộng thích hợp (sẽ bằng 0 trong trường hợp e là số không).


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 ứng tốt:

Các macro này thực hiện một phép thử thời gian biên dịch, trong khi khẳng định () là một thử nghiệm thời gian chạy.

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


1532
2018-02-10 15:04



các biến thể gần đây của C ++ hoặc C tiêu chuẩn có một cái gì đó như static_assert cho các mục đích liên quan. - Basile Starynkevitch
@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 vào bộ xử lý trước. Hack này hoạt động cho bất kỳ đánh giá nào có thể truy cập được tới trình biên dịch. - Ed Staub
Nhân Linux không sử dụng C ++, ít nhất là trong khi Linus vẫn còn sống. - Mark Ransom
Điều đáng chú ý là !!e không đánh giá về số không "hoặc số không dương", mà là 0 hoặc không, cụ thể. Các biểu thức Boolean trong C được định nghĩa để luôn luôn đánh giá bằng 0 hoặc một. - Dolda2000
Tôi cũng tự hỏi về điều đó, nhất là khi tôi nói rõ ràng là 0 hoặc 1 nhưng tôi cho rằng bạn đã viết như một nhà toán học thuần túy và tuyên bố chỉ đủ để chứng minh quan điểm, và không cần thiết. - David Heffernan


Các : là một bitfield. Như cho !!, đó là -sự phủ định kép hợp lý và vì vậy lợi nhuận 0 cho sai hoặc 1 cho đúng. Và - là dấu trừ, nghĩa là phủ định số học.

Đó là tất cả chỉ là một thủ thuật để có được trình biên dịch để barf trên đầu vào không hợp lệ.

Xem xét BUILD_BUG_ON_ZERO. Khi nào -!!(e) đánh giá đến một giá trị âm, tạo ra một lỗi biên dịch. Nếu không thì -!!(e) đánh giá là 0 và bitfield 0 chiều rộng có kích thước bằng 0. Và do đó macro đánh giá thành size_t với giá trị 0.

Tên là yếu trong quan điểm của tôi bởi vì việc xây dựng trong thực tế không thành công khi đầu vào là không phải số không.

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


236
2018-02-10 14:54



Là sizeof(struct { int:0; }) nghiêm chỉnh phù hợp? - ouah
Tại sao kết quả nói chung là 0? A struct chỉ với một bitfield trống, đúng, nhưng tôi không nghĩ rằng cấu trúc với kích thước 0 được cho phép. Ví dụ: nếu bạn muốn tạo một mảng thuộc loại đó, các phần tử mảng riêng lẻ vẫn phải có các địa chỉ khác nhau, phải không? - Jens Gustedt
họ thực sự không quan tâm như khi họ sử dụng phần mở rộng GNU, họ vô hiệu hóa quy tắc bí danh nghiêm ngặt và không xem xét số nguyên tràn như UB. Nhưng tôi đã tự hỏi nếu điều này là nghiêm chỉnh phù hợp với C. - ouah
@ouah liên quan đến bitfield không có độ dài không tên, xem tại đây: stackoverflow.com/questions/4297095/… - David Heffernan
@DavidHeffernan thực sự C cho phép trường bit không được đặt tên của 0 chiều rộng, nhưng không nếu không có thành viên có tên 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í dụ sizeof (struct {int a:1; int:0;}) tuân thủ nghiêm ngặt nhưng sizeof(struct { int:0; }) không phải là (hành vi không xác định). - ouah


Một số người dường như khó hiểu các macro này assert().

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


149
2018-02-10 15:37



@ Lundin Bạn đang nói đùa à? Rõ ràng nó là tốt hơn để có một lỗi trình biên dịch trong của bạn hạt nhân, hơn là để nó chưa được khám phá cho đến khi bạn khởi động một tên lửa hành trình có hệ thống hướng dẫn khởi tạo một hạt nhân hoảng loạn. - John Feminella
@ JohnFeminella: Vâng tôi đang nói đùa, người đã từng nghe nói về những người thực sự cố gắng chạy mã họ đã viết! Thử nghiệm - hah! Nó biên dịch, gửi nó! - Lundin
@ Lundin: Nó không phải là mỉa mai và mỉa mai nếu bạn vẫn nghĩ rằng nó không quan trọng. Nó không thành vấn đề. Điều quan trọng hơn rất nhiều, và một lỗi biên dịch là dặm trước một thời gian chạy một. Và nếu bạn nghĩ rằng tất cả nó cần để sửa lỗi thời gian chạy là chạy mã của bạn ít nhất một lần ... cũng thấy bình luận của tôi ở trên. - gparent
@ Lundin: Thực ra tôi hiểu tất cả những điều này, bạn không thể làm rõ điều đó. Và dù bằng cách nào, nó cũng sẽ ổn hơn thời gian biên dịch so với thời gian chạy, khiến toàn bộ lập luận không liên quan. - gparent
@ Lundin: Chỉ khi assert được thực hiện. Nó không phải là không phổ biến để nhận ra rằng các bài kiểm tra của tôi bị mất ít nhất một codepath. - Mooing Duck


Vâng, tôi khá ngạc nhiên rằng các lựa chọn thay thế cho cú pháp này chưa được đề cập đến. Một cơ chế chung (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 cuộc gọi hàm nếu xác nhận của bạn là chính xác.

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

Trong khi cơ chế này hoạt động (miễn là tối ưu hóa được kích hoạt) nó có nhược điểm của việc 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_something_bad (). Đó là lý do tại sao các nhà phát triển hạt 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).

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

#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 tôi có một macro được gọi là compiletime_assert sử dụng tính năng này và hầu hết các macro trong bug.h đã được cập nhật cho phù hợp. Tuy nhiên, macro này không thể được sử dụng làm bộ khởi tạo. Tuy nhiên, bằng cách sử dụng biểu thức câu lệnh (một GCC C-mở rộng), 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á thông 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 một lỗi biên dịch thời gian nói rằng "Tôi đã nói với bạn không cho tôi một năm!" nếu biểu thức đánh giá năm hoặc không phải là hằng số biên dịch.

Vậy tại sao chúng ta không sử dụng nó 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 các biểu thức câu lệnh, bao gồm việc sử dụng chúng như là các khởi tạo hằng số (cho hằng số enum, chiều 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 (ví dụ, có thể được đánh giá đầy đủ) tại thời gian biên dịch và nếu không thì sẽ __builtin_constant_p() kiểm tra). Hơn nữa, chúng không thể được sử dụng bên ngoài một thân hàm.

Hy vọng rằ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 tuyên bố liên tục được sử dụng như là các bộ khởi tạo không đổi. Thách thức ở đây là đặc tả ngôn ngữ xác định biểu thức hằng số hợp pháp là gì. C ++ 11 thêm từ khóa constexpr chỉ cho kiểu hoặc điều này, nhưng không tồn tại đối tác trong C11. Trong khi C11 đã nhận được xác nhận tĩnh, mà sẽ giải quyết một phần của vấn đề này, nó sẽ không giải quyết tất cả những thiếu sót này. Vì vậy, tôi hy vọng rằng gcc có thể làm cho một chức năng constexpr có sẵn như là một phần 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 tuyên bố et. al.


42
2018-06-27 08:21



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 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 kiểu size_t - Wiz
@ Wiz Vâng, tôi biết điều này. Có lẽ điều này hơi dài dòng và có lẽ tôi cần phải 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 tôi vẫn sử dụng bitfield có kích thước âm. 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ẽ mở các tùy chọn khác. - Daniel Santos
Dù sao chúng tôi không thể sử dụng các macro cho một biến. đúng? error: bit-field ‘<anonymous>’ width not an integer constant Chỉ cho phép các hằng số của nó. Vậy, sử dụng là gì? - Karthik
@ Karthik Tìm kiếm các nguồn của hạt nhân Linux để xem tại sao nó được sử dụng. - Daniel Santos


Nó đang tạo ra một kích thước 0 bitfield nếu điều kiện là sai, nhưng kích thước -1 (-!!1) bitfield nếu điều kiện là true / khác không. Trong trường hợp trước đây, không có lỗi và cấu trúc đượ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à không có thứ như kích thước -1 bitfield được tạo ra, tất nhiên).


31
2018-02-10 14:54



Thực ra nó trả về size_t với giá trị 0 trong trường hợp điều kiện là đúng. - David Heffernan


 Linux Kernel :   

/* 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); }))

-1
2018-06-21 07:18



Cảm ơn bạn về đoạn mã này, đoạn mã này có thể cung cấp một số trợ giúp tức thì, có giới hạn. A giải thích hợp lý sẽ cải thiện đáng kể giá trị lâu dài của nó bằng cách hiển thị tại sao đây là một giải pháp tốt cho vấn đề, và sẽ làm cho nó hữu ích hơn cho độc giả tương lai với các câu hỏi tương tự khác. Xin vui lòng chỉnh sửa câu trả lời của bạn để thêm một số giải thích, bao gồm các giả định bạn đã thực hiện. - Machavity