Що ":-!!" в коді С?


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 /> зворотний не не для даного Integer e, тому змінна може бути або 0, або 1.
CyrillC

69
винна гіта говорить нам, що саме цю форму статичного твердження ввів Ян Беуліч у 8c87df4 . Очевидно, що у нього були вагомі причини (див. Повідомлення про вчинення).
Ніклас Б.

55
@Lundin: assert () НЕ викликає помилку під час компіляції. У цьому вся суть вищезгаданої конструкції.
Chris Pacejo

4
@GreweKokkor Не будьте наївні, Linux занадто великий, щоб одна людина впоралася з усім. У Лінуса є свої лейтенанти, і вони мають своїх, які підштовхують зміни та вдосконалення знизу вгору. Лінус лише вирішує, хоче він чи ні, але до певної міри довіряє колегам. Якщо ви хочете дізнатися більше про те, як працює розподілена система у відкритому середовищі, перегляньте відео YouTube: youtube.com/watch?v=4XpnKHJAok8 (Це дуже цікава розмова).
Томаш Прузіна

3
@cpcloud, sizeof"оцінює" тип, тільки не значення. Його тип недійсний у цьому випадку.
Вінстон Еверт

Відповіді:


1692

Це, по суті, спосіб перевірити, чи можна вираз e оцінити як 0, а якщо ні, то не вдалося зібрати .

Макрос дещо неправильно названий; це має бути щось більше BUILD_BUG_OR_ZERO, ніж ...ON_ZERO. (Були періодичні дискусії про те, чи це заплутане ім’я .)

Ви повинні прочитати такий вираз:

sizeof(struct { int: -!!(e); }))
  1. (e): Обчислити вираз e.

  2. !!(e): Логічно заперечуйте двічі: 0if e == 0; інакше 1.

  3. -!!(e): Числове заперечення виразу з кроку 2: 0якщо воно було 0; інакше -1.

  4. struct{int: -!!(0);} --> struct{int: 0;}: Якщо він дорівнював нулю, то ми оголосимо структуру з анонімним цілим бітовим полем, яке має нульову ширину. Все добре, і ми проходимо як нормально.

  5. struct{int: -!!(1);} --> struct{int: -1;}: З іншого боку, якщо вона не дорівнює нулю, то це буде деяке від’ємне число. Оголошення будь-якого бітового поля з негативною шириною - помилка компіляції.

Таким чином, ми або закінчимо бітполе, яке має ширину 0 в структурі, що добре, або бітове поле з негативною шириною, що є помилкою компіляції. Потім беремо sizeofце поле, тому отримуємо а size_tз відповідною шириною (яка буде нульовою у випадку, коли eдорівнює нулю).


Деякі люди запитували: Чому б не просто використовувати assert?

Відповідь keithmo тут має хорошу відповідь:

Ці макроси реалізують тест часу компіляції, тоді як assert () - тест часу виконання.

Точно правильно. Ви не хочете виявляти проблеми у вашому ядрі під час виконання, які могли бути спіймані раніше! Це критична частина операційної системи. Якою б мірою проблеми були виявлені під час компіляції, тим краще.


5
@weston Багато різних місць. Побачте самі!
Джон Фемінелла

166
останні варіанти стандартів C ++ або C мають щось подібне static_assertдля споріднених цілей.
Василь Старинкевич

54
@Lundin - #error вимагає використання 3 рядків коду # if / # error / # endif, і буде працювати лише для оцінок, доступних попередньому процесору. Цей хак працює для будь-якої оцінки, доступної компілятору.
Ед Штауб

236
Ядро Linux не використовує C ++, принаймні, не поки Лінус ще живий.
Марк Викуп

6
@ Dolda2000: " Булеві вирази в C визначені завжди для оцінки нуля чи одиниці " - Не точно. Ці оператори , які дають «логічні» логічно результати ( !, <, >, <=, >=, ==, !=, &&, ||) завжди дають 0 або 1. Інших виразів можуть давати результати , які можуть бути використані в якості умов, але є просто нуль або НЕ нуль; наприклад, isdigit(c)де cє цифра, може дати будь -яке ненульове значення (яке в умові вважається істинним).
Кіт Томпсон

256

Це :бітфілд. Що стосується !!, це логічне подвійне заперечення і тому повертається 0для хибного або 1для істинного. І -знак мінус, тобто арифметичне заперечення.

Це все лише хитрість, щоб змусити компілятора переграти недійсні входи.

Розглянемо BUILD_BUG_ON_ZERO. Коли -!!(e)оцінюється на негативне значення, це створює помилку компіляції. В іншому випадку -!!(e)оцінюється до 0, а бітове поле шириною 0 має розмір 0. А отже, макрос оцінює до size_tзначення зі значенням 0.

На мій погляд, назва слабка, оскільки збірка насправді виходить з ладу, коли вхід не дорівнює нулю.

BUILD_BUG_ON_NULLдуже схожий, але отримує вказівник, а не an int.


14
це sizeof(struct { int:0; })строго в відповідно?
оуа

7
Чому взагалі результат був би 0? A structз порожнім бітовим полем, правда, але я не думаю, що структура з розміром 0 дозволена. Наприклад, якщо ви створили масив цього типу, окремі елементи масиву все одно повинні мати різні адреси, ні?
Йенс Гуведт

2
вони насправді не хвилюються, оскільки вони використовують розширення GNU, вони вимикають правило суворого псевдоніму та не розглядають цілі переповнення як UB. Але мені було цікаво, чи це суворо відповідає С.
оуа

3
@ouah щодо неназваних бітових полів нульової довжини дивіться тут: stackoverflow.com/questions/4297095/…
Девід Хеффернан

9
@DavidHeffernan насправді C дозволяє немедним бітовим полем 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; })це не так (невизначена поведінка).
оуа

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 C), ви можете!

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

Цей макрос точно раз оцінить його параметр (у випадку, якщо він має побічні ефекти) та створить помилку під час компіляції, яка говорить "Я сказав вам не давати мені п'ять!" якщо вираз оцінюється на п’ять або не є константою часу компіляції.

То чому ми не використовуємо це замість бітових полів негативного розміру? На жаль, в даний час існує багато обмежень щодо використання виразів заяви, включаючи їх використання в якості постійних ініціалізаторів (для констант перерахунку, ширини бітового поля тощо), навіть якщо вираз твердження повністю постійний сам (тобто може бути повністю оцінений під час компіляції та іншим чином проходить __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 Так, я це знаю. Можливо, це було дещо багатослівним, і, можливо, мені потрібно заново відвідати формулювання, але моя суть полягала в тому, щоб вивчити різні механізми статичних тверджень і показати, чому ми все ще використовуємо бітові поля негативного розміру. Якщо коротко, якщо ми отримаємо механізм постійного вираження висловлювань, у нас будуть відкриті інші варіанти.
Даніель Сантос

У будь-якому випадку ми не можемо використовувати ці макроси для змінної. правильно? error: bit-field ‘<anonymous>’ width not an integer constantЦе дозволяють лише константи. Отже, в чому користь?
Karthik Raj Паланічамі

1
@Karthik Шукайте джерела ядра Linux, щоб побачити, чому воно використовується.
Даніель Сантос

@supercat Я не бачу, як ваш коментар взагалі пов'язаний. Чи можете ви, будь ласка, переглянути його, краще пояснити, що ви маєте на увазі чи видалити?
Даніель Сантос

36

Це створює 0бітфілд розміру, якщо умова хибний, але бітове поле розміру -1( -!!1), якщо умова є істинним / ненульовим. У першому випадку помилки немає і структура ініціалізується з int-членом. В останньому випадку виникає помилка компіляції (і -1, звичайно, не створюється така річ, як бітфілд розміру ).


3
Насправді він повертає size_tзначення зі значенням 0 у випадку, якщо умова є правдою.
Девід Геффернан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.