Письмо
Отправитель: Мартин УэкерЗдравствуй Линус,
Дата: Tue, 20 Mar 2018 22:13:35 +0000
Тема: Обнаружение целочисленных константных выражений в макросе
У меня появилась идея:
Тест для целочисленных константных выражений, который возвращает само целочисленное константное выражение (integer constant expression, ICE), которое должно подходить для передачи в
__builtin_choose_expr
, и выглядит следующим образом:#define ICE_P(x) (sizeof(int) == sizeof(*(1 ? ((void*)((x) * 0l)) : (int*)1)))
Кстати, в этом выражении само
x
не вычисляется в gcc, хотя это и не гарантируется стандартом (я не проверял этот факт в старых версиях gcc.)Ответ Линуса Торвальдса
Отправитель: Линус Торвальдс <>
Дата: Tue, 20 Mar 2018 16:08:30 -0700
Тема: Re: Обнаружение целочисленных константных выражений в макросе
On Tue, Mar 20, 2018 at 3:13 PM, Мартин УэкерНет, это не «идея».
<Martin.Uecker@med.uni-goettingen.de> написал:
У меня появилась идея:
Это либо работа гения, либо напрочь больного на голову.
До конца пока не уверен, поэтому не могу сказать с точностью.
Тест для целочисленных константных выражений, который возвращает само целочисленное константное выражение, которое должно подходить для передачи в __builtin_choose_expr, и выглядит следующим образом:ОК, здесь я вижу, что
#define ICE_P(x) (sizeof(int) == sizeof(*(1 ? ((void*)((x) * 0l)) : (int*)1)))
(void *)((x)*0l))
становится NULL
когда x
– это ICE
. Хорошо. С константой мы имеем:sizeof( 1 ? NULL : (int *) 1)
и правило здесь следующее — если одна из сторон тернарного оператора с указателями является
NULL
, то её конечный результат — это другой тип (int *)
.Так что да, выражение выше возвращает
sizeof(int)
.И если оно не ICE, то первый указатель всё ещё типа (void*), но он не
NULL
.И да, правила приведения типов для тернарного оператора с двумя указателями, каждый из которых не является
NULL
, различные — поэтому теперь оно возвращает "void *"
.Итак, теперь конечный результат — это
(sizeof(*(void *)(x))
, что в gcc как правило отличается от int.Итак, здесь я наблюдаю две проблемы:
"sizeof(*(void *)1)"
не обязательно строго определено. Для gcc это 1. Это может стать причиной предупреждений (warnings).- это поломает мозг каждому, кому на глаза попадется данное выражение.
Однако, обе эти проблемы могут не иметь особого значения, и всё это может быть нормой.
Кстати, в этом выражении само x
не вычисляется в gcc, хотя это и не гарантируется стандартом (я не проверял этого в старых версиях gcc.)
О, как по мне, стандартом именно что гарантируется, что оператор sizeof()
не вычисляет значение аргумента, только его тип.Я в восторге от вашего по-настоящему удивительного и отвратительного «хака». Он представляет собой самое настоящее произведение искусства.
Я уверен, что это не будет работать или вызовет предупреждения по разным причинам, но
это по-прежнему просто прекрасно.
Линус
Объяснение
Давайте постараемся разобраться в том, что происходит в данном коде.
#define ICE_P(x) (sizeof(int) == sizeof(*(1 ? ((void*)((x) * 0l)) : (int*)1)))
Мы определяем макрос
ICE_P(x)
. P
— это, согласно правилам именования, лисповатый предикат. ICE обозначает целочисленное константное выражение. Мы хотим вернуть true
, если x
— это целочисленное константное выражение, и false
— в другом случае.Это выражение будет
true
, если правая часть сравнения равна sizeof(int)
. Попробуем развернуть её.sizeof(*(1 ? ((void*)((x) * 0l)) : (int*)1))
Это выражение возвращает размер типа, на который указывает тернарное выражение. Копаем глубже.
1 ? ((void*)((x) * 0l)) : (int*)1
Понятное дело, левая часть всегда возвращается, поскольку 1 — это всегда
true
. Как разъясняет Линус, когда x
— это ICE, левая сторона становится NULL
. Получается, у нас есть два возможных варианта:Когда
x
это ICE: 1 ? ((void*)(NULL)) : (int*)1
Когда
x
это не ICE: 1 ? ((void*)(NOT-NULL)) : (int*)1
Единственная разница состоит в том, является ли
void*
слева NULL
или нет.Если оно
NULL
(x — это ICE), выражение возвращает тип int*
Если оно не
NULL
(x — это не ICE), выражение возвращает void*
По сути, тернарное выражение может превратить
NULL void *
в int *
, но когда void *
— не NULL
, вместо этого превратит int * в
void *
. Теперь мы можем вернуться к оригинальному выражению, и мы получаем следующее:Если
x
это ICE: sizeof(int) == sizeof(*(int *))
Когда
x
это не ICE: sizeof(int) == sizeof(*(void *))
Разыменование void * не является валидной операцией, но sizeof — это магия, оно полностью вычисляется во время компиляции. В gcc код
sizeof(*(void *))
даёт 1.Вот пример кода, позволяющий протестировать данный макрос,
icep.c
:/*
компилируем и запускаем: gcc icep.c -o icep && ./icep
ожидаемый вывод:
$ gcc icep.c -o icep && ./icep
ICE_P(1): 1
ICE_P('c'): 1
ICE_P(rand()): 0
*/
#include <stdio.h>
#include <stdlib.h>
#define ICE_P(x) (sizeof(int) == sizeof(*(1 ? ((void*)((x) * 0l)) : (int*)1)))
#define CHECK(x) printf("ICE_P(%s): %d\n", #x, ICE_P(x))
int main()
{
CHECK(1);
CHECK('c');
CHECK(rand());
return 0;
}
Дополнительное объяснение
Ключевое выражение здесь — это всего лишь
x * 0
. Если x
— это целочисленная константа, компилятор может произвести вычисление, и целое на ноль — это ноль. Если x
— это не целочисленная константа, то компилятор не может выполнить это вычисление, и неизвестно, является ли оно нулем. Этот результат приводится к «пустому» указателю (void pointer). Вот как мы узнаем, NULL
или нет (поскольку void pointer к нулю — это определение NULL
).Еще один ключ к пониманию этого выражения — это тип
a ? b : c
. Понятно, что b
и c
могут иметь различные типы, и в этом случае, компилятор должен выяснить «общий» тип этих выражений. Здесь c
— это явно указатель на int
. Но NULL
совместим с другими типами указателей. Так что если b
— это NULL
, тогда общий тип — это int*
, поскольку он описывает оба выражения. Однако, если статически неизвестно, является ли b NULL
, то единственным типом, который подходит void*
и int*
— это void*
.Это приводит нас к тому, что мы делаем
sizeof(*(void*))
, когда x
— это не целочисленное константное выражение, и sizeof(*(int*))
, когда x
— это оно самое. Комментарии (16)
NeoCode
29.03.2018 15:19Как раз для этого нужно API для доступа к синтаксическому дереву во время комплляции (т.е. синтаксические макросы). Но в cи (и особенно c++) не ищут легких путей:)
timon_aeg
29.03.2018 16:30+5Чем это может быть полезно в народном хозяйстве?
imwode
29.03.2018 20:52В тексте письма написано что-то вроде «должно подходить для передачи в __builtin_choose_expr». Не уверен, как это расшифровывается
staticlab
29.03.2018 23:49https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html
Built-in Function: type __builtin_choose_expr (const_exp, exp1, exp2)
You can use the built-in function__builtin_choose_expr
to evaluate code depending on the value of a constant expression. This built-in function returns exp1 if const_exp, which is an integer constant expression, is nonzero. Otherwise it returns exp2.
This built-in function is analogous to the ‘? :
’ operator in C, except that the expression returned has its type unaltered by promotion rules. Also, the built-in function does not evaluate the expression that is not chosen. For example, if const_exp evaluates to true, exp2 is not evaluated even if it has side-effects.
This built-in function can return an lvalue if the chosen argument is an lvalue.
Deosis
30.03.2018 08:08Если в макросе х — не константа, а чистая функция, то имеет ли право компилятор заменить x * 0 на 0, так как для внешнего наблюдателя ничего не изменится?
abusalimov
30.03.2018 14:25В качестве оптимизации да. Но поскольку оптимизации не должны изменять семантику программы, на вывод типов это не влияет. Если бы в C были constexpr-функции из C++, тогда другое дело (наверное).
Nick_Shl
30.03.2018 16:17Имеет! Из статьи:
стандартом именно что гарантируется, что оператор sizeof() не вычисляет значение аргумента, только его тип
Т.е. поскольку вызов функции окажется внутри sizeof, то никто эту функцию звать не будет. Да и не возможно это — позвать ее на этапе компиляции, когда вычисляется sizeof.
Nick_Shl
30.03.2018 08:18А меня тут ругали, что я ругаю undefined behavior за то, что не всегда он должен быть undefined. А тут и положение, что компилятор не поумнее и ее научится переменные на 0 умножать на этапе компиляции, и взятие sizeof от void...
Sklott
Не специалист в тонкостях C, так что может кто-нибудь объяснить: если приводить 0 не к void *, а например к long *, то это уже не будет считаться NULL-ом?