Что такое ":-!!" в коде 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); }))

Что делает :-!!?

chmurli
источник
2
- Одинарный минус <br />! Логическое НЕ <br /> не обратное, не заданное целое число e, поэтому переменная может быть либо 0, либо 1.
CyrillC
69
Git Blame говорит нам, что эта конкретная форма статического утверждения была введена Яном Белихом в 8c87df4 . Очевидно, у него были веские причины для этого (см. Сообщение о коммите).
Никлас Б.
55
@Lundin: assert () НЕ вызывает ошибку во время компиляции. В этом весь смысл вышеупомянутой конструкции.
Крис Пачехо
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)Логически отрицать дважды: 0если 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это поле, поэтому мы получаем a size_tс соответствующей шириной (которая будет равна нулю в случае, когда eноль).


Некоторые люди спрашивают: почему бы просто не использовать assert?

Ответ Кейтмо здесь имеет хороший ответ:

Эти макросы реализуют тест во время компиляции, а 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. И, следовательно, макрос оценивается как a size_tсо значением 0.

На мой взгляд, это слабое имя, потому что на самом деле сборка завершается неудачно, когда ввод не равен нулю.

BUILD_BUG_ON_NULLочень похоже, но дает указатель, а не int.

Дэвид Хеффернан
источник
14
это sizeof(struct { int:0; })строго в соответствии?
ouah
7
Зачем вообще результат 0? A structтолько с пустым битовым полем, правда, но я не думаю, что структура с размером 0 допускается. Например, если вы создадите массив этого типа, отдельные элементы массива все равно должны иметь разные адреса, нет?
Йенс Гастедт
2
на самом деле им все равно, поскольку они используют расширения GNU, они отключают строгое правило псевдонимов и не рассматривают целочисленные переполнения как UB. Но мне было интересно, строго ли это соответствует C.
Офф
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; })это не так (неопределенное поведение).
ouah
168

Некоторые люди, кажется, путают эти макросы с assert().

Эти макросы реализуют тест во время компиляции, в то время assert()как это тест во время выполнения.

keithmo
источник
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были соответствующим образом обновлены. Тем не менее, этот макрос не может быть использован в качестве инициализатора. Однако, используя выражения выражений (другое C-расширение 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;                                                    \
    })

Этот макрос оценит свой параметр ровно один раз (в случае, если у него есть побочные эффекты) и создаст ошибку во время компиляции, которая говорит: «Я сказал вам не давать мне пять!» если выражение оценивается как пять или не является константой времени компиляции.

Так почему же мы не используем это вместо битовых полей отрицательного размера? Увы, в настоящее время существует много ограничений на использование выражений операторов, в том числе их использование в качестве инициализаторов констант (для констант перечисления, ширины битового поля и т. Д.), Даже если выражение оператора полностью само по себе (т. Е. Может быть полностью оценено). во время компиляции и в противном случае проходит __builtin_constant_p()тест). Кроме того, они не могут использоваться вне функционального тела.

Надеемся, что GCC вскоре исправит эти недостатки и позволит использовать выражения константных выражений в качестве инициализаторов констант. Проблема здесь заключается в спецификации языка, определяющей, что является законным постоянным выражением. C ++ 11 добавил ключевое слово constexpr только для этого типа или вещи, но в C11 не существует аналога. Хотя C11 получил статические утверждения, которые решат часть этой проблемы, он не решит все эти недостатки. Поэтому я надеюсь, что gcc может сделать функциональность constexpr доступной как расширение через -std = gnuc99 & -std = gnuc11 или что-то подобное и разрешить его использование в выражениях выражений et. и др.

Даниэль Сантос
источник
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Это позволяет только константы. Итак, какая польза?
Картик Радж Паланишами
1
@Kartik Поиск источников ядра Linux, чтобы понять, почему оно используется.
Даниэль Сантос
@supercat Я не понимаю, как твой комментарий вообще связан. Можете ли вы пересмотреть это, лучше объяснить, что вы имеете в виду или удалить это?
Даниэль Сантос
36

Он создает 0битовое поле размера, если условие ложно, но битовое поле размера -1( -!!1), если условие истинно / не равно нулю. В первом случае ошибки нет, и структура инициализируется с помощью члена int. В последнем случае возникает ошибка компиляции (и, -1конечно, не создается такая вещь, как битовое поле размера ).

Мэтт Филлипс
источник
3
На самом деле он возвращает size_tзначение 0, если условие истинно.
Дэвид Хеффернан