Обнаруживаем целочисленные константные выражения в макросе [вместе с Линусом]

в 11:31, , рубрики: C, linus torvalds, lkml, Программирование, системное программирование

Вашему вниманию предлагается перевод недавнего письма по поводу неоднозначной идеи из рассылки Linux Kernel Mailing List, вызвавшей традиционную реакцию Линуса Торвальдса. Необходимые для понимания пояснения предоставлены в конце поста.

Письмо

Отправитель: Мартин Уэкер
Дата: 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): %dn", #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 — это оно самое.

Автор: HotWaterMusic

Источник

Поделиться

* - обязательные к заполнению поля