В какой степени препроцессор C учитывает суффиксы целочисленных литералов?

Сегодня я наткнулся на что-то вроде этого:

#define FOO 2u

#if (FOO == 2)
  unsigned int foo = FOO;
#endif

Независимо от того, почему код такой, какой он есть (давайте не будем сомневаться в why), мне было интересно, в какой степени препроцессор может обрабатывать суффиксы целочисленных литералов. На самом деле я был удивлен, что это вообще работает. После некоторых экспериментов с GCC и C99 с этим кодом...

#include <stdio.h>

int main()
{
  #if (1u == 1)
    printf("1u == 1\n");
  #endif

  #if (1u + 1l == 2ll)
    printf("1u + 1l == 2ll\n");
  #endif

  #if (1ull - 2u == -1)
    printf("1ull - 2u == -1\n");
  #endif

  #if (1u - 2u == 0xFFFFFFFFFFFFFFFF)
    printf("1u - 2u == 0xFFFFFFFFFFFFFFFF\n");
  #endif

  #if (-1 == 0xFFFFFFFFFFFFFFFF)
    printf("-1 == 0xFFFFFFFFFFFFFFFF\n");
  #endif

  #if (-1l == 0xFFFFFFFFFFFFFFFF)
    printf("-1l == 0xFFFFFFFFFFFFFFFF\n");
  #endif

  #if (-1ll == 0xFFFFFFFFFFFFFFFF)
    printf("-1ll == 0xFFFFFFFFFFFFFFFF\n");
  #endif
}

... который просто печатает все операторы:

1u == 1
1u + 1l == 2ll
1ull - 2u == -1
1u - 2u == 0xFFFFFFFFFFFFFFFF
-1 == 0xFFFFFFFFFFFFFFFF
-1l == 0xFFFFFFFFFFFFFFFF
-1ll == 0xFFFFFFFFFFFFFFFF

... Я предполагаю, что препроцессор просто полностью игнорирует суффиксы целочисленных литералов и, вероятно, всегда выполняет арифметику и сравнения в собственном целочисленном размере, в данном случае 64-битном?

Итак, вот что я хотел бы знать:

  1. В какой степени препроцессор учитывает суффиксы целочисленных литералов? Или просто игнорирует их?
  2. Существуют ли какие-либо зависимости или различное поведение в разных средах, например. разные компиляторы, C и C++, 32-битные и 64-битные машины и т. д.? То есть от чего зависит поведение препроцессора?
  3. Где все это указано/задокументировано?

Я хотел узнать сам и проверил Википедия и стандарт C (рабочий документ). Я нашел информацию о целочисленных суффиксах и информацию о препроцессоре, но ничего о их комбинации. Очевидно, я также гуглил, но не получил никаких полезных результатов.

Я видел этот вопрос Stack Overflow, в котором поясняется, где указывается должен, но пока не нашел ответа на свои вопросы.

C11 §6.10.1 Условное включение ¶4: … Для целей этого преобразования и оценки токенов все типы целых чисел со знаком и все типы целых чисел без знака действуют так, как если бы они имели то же представление, что и типы intmax_t и uintmax_t, определенные в заголовке <stdint.h>, соответственно.

Jonathan Leffler 21.05.2019 20:24
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
13
1
690
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

C 2018 6.10.1 касается условного включения (#if и связанных операторов и оператора defined). В пункте 1 говорится:

The expression that controls conditional inclusion shall be an integer constant expression except that: identifiers (including those lexically identical to keywords) are interpreted as described below; and it may contain unary operator expressions of the form

definedidentifier

or

defined(identifier)

Целочисленное постоянное выражение определен в 6.6 6:

An integer constant expression shall have integer type and shall only have operands that are integer constants, enumeration constants, character constants, sizeof expressions whose results are integer constants, _Alignof expressions, and floating constants that are the immediate operands of casts. Cast operators in an integer constant expression shall only convert arithmetic types to integer types, except as part of an operand to the sizeof or _Alignof operator.

Этот параграф предназначен для C в целом, а не только для препроцессора. Таким образом, выражения, которые могут появляться в операторах #if, такие же, как целочисленные константные выражения, которые обычно могут появляться в C. Однако, как указано в приведенной выше цитате, sizeof и _Alignof — это просто идентификаторы; они не распознаются как операторы C. В частности, 6.10.1 4 говорит нам:

… After all replacements due to macro expansion and the defined unary operator have been performed, all remaining identifiers (including those lexically identical to keywords) are replaced with the pp-number 0,…

Итак, где sizeof или _Alignof появляются в выражении #if, оно становится 0. Таким образом, выражение #if может иметь только операнды, которые являются константами и выражениями defined.

Далее в абзаце 4 говорится:

… The resulting tokens compose the controlling constant expression which is evaluated according to the rules of 6.6. For the purposes of this token conversion and evaluation, all signed integer types and all unsigned integer types act as if they have the same representation as, respectively, the types intmax_t and uintmax_t defined in the header <stdint.h>.…

6.6 — это раздел для константных выражений.

Таким образом, компилятор будет принимать целочисленные суффиксы в выражениях #if, и это не зависит от реализации C (для суффиксов, необходимых в ядре языка C; реализации могут разрешать расширения). Однако вся арифметика будет выполняться с использованием intmax_t или uintmax_t, и это зависит от реализации. Если ваши выражения не зависят от ширины целых чисел выше минимально необходимой1, они должны оцениваться одинаково в любой реализации C.

Кроме того, в параграфе 4 говорится, что могут быть некоторые вариации с символьными константами и значениями, которые я опускаю здесь, поскольку они не имеют отношения к этому вопросу.

Сноска

1intmax_t обозначает тип со знаком, способный представлять любое значение любого целочисленного типа со знаком (7.20.1.5 1), а long long int — тип со знаком, который должен быть не менее 64 бит (5.2.4.2.1 1), поэтому любая соответствующая реализация C должен обеспечивать 64-битную целочисленную арифметику в препроцессоре.

О Таким образом, выражения, которые могут появляться в операторах #if, совпадают с целочисленными константными выражениями, которые обычно могут появляться в C.: Думаю, не совсем. Сноска в главе «Константные выражения» читается как Дополнительные ограничения, которые применяются к целочисленным константным выражениям, используемым в директивах предварительной обработки условного включения, обсуждаются в 6.10.1., а затем в сноске к 6.10.1: Поскольку управляющее постоянное выражение оценивается на этапе трансляции 4, все идентификаторы либо являются, либо не являются именами макросов — просто отсутствуют ключевые слова, константы перечисления и т. д..

Michi 21.05.2019 23:10

@Michi: Это обсуждается, начиная с предложения сразу после того, как вы остановили кавычку, начиная с «Однако» и заканчивая «Таким образом, выражение #if может иметь только операнды, которые являются константами и определенными выражениями».

Eric Postpischil 22.05.2019 02:39

Хорошо, теперь я понимаю, что вы имеете в виду. Я был сбит с толку, просто прочитав sizeof и _Alignof. Раньше я не понимал всего смысла. Во всяком случае, я думаю, что мы имеем в виду одно и то же.

Michi 22.05.2019 12:22

Как я отметил в комментарии, это определено в стандарте C. Вот полный текст §6.10.1 ¶4 (и две сноски):

C11 §6.10.1 Conditional inclusion

¶4 Prior to evaluation, macro invocations in the list of preprocessing tokens that will become the controlling constant expression are replaced (except for those macro names modified by the defined unary operator), just as in normal text. If the token defined is generated as a result of this replacement process or use of the defined unary operator does not match one of the two specified forms prior to macro replacement, the behavior is undefined. After all replacements due to macro expansion and the defined unary operator have been performed, all remaining identifiers (including those lexically identical to keywords) are replaced with the pp-number 0, and then each preprocessing token is converted into a token. The resulting tokens compose the controlling constant expression which is evaluated according to the rules of 6.6. For the purposes of this token conversion and evaluation, all signed integer types and all unsigned integer types act as if they have the same representation as, respectively, the types intmax_t and uintmax_t defined in the header <stdint.h>.167) This includes interpreting character constants, which may involve converting escape sequences into execution character set members. Whether the numeric value for these character constants matches the value obtained when an identical character constant occurs in an expression (other than within a #if or #elif directive) is implementation-defined.168) Also, whether a single-character character constant may have a negative value is implementation-defined.

167 167) Thus, on an implementation where INT_MAX is 0x7FFF and UINT_MAX is 0xFFFF, the constant 0x8000 is signed and positive within a #if expression even though it would be unsigned in translation phase 7.

168 Thus, the constant expression in the following #if directive and if statement is not guaranteed to evaluate to the same value in these two contexts.

#if 'z' - 'a' == 25
if ('z' - 'a' == 25)

Раздел 6.6 — §6.6 Постоянные выражения, в котором подробно описаны различия между полными выражениями в разделе §6.5 Выражения и константными выражениями.

По сути, препроцессор в значительной степени игнорирует суффиксы. Шестнадцатеричные константы не имеют знака. Показанные вами результаты следует ожидать на машине, где intmax_t и uintmax_t — 64-битные величины. Если бы ограничения на intmax_t и uintmax_t были больше, некоторые выражения могли бы измениться.

Препроцессор не может игнорировать суффиксы. Это может игнорировать их эффекты ширины, но не подписанность.

Eric Postpischil 21.05.2019 20:55
Ответ принят как подходящий
  1. To which degree does the preprocessor regard integer literal suffixes? Or does it just ignore them?

Суффиксы типов целочисленных констант не имеют значения для препроцессора по своей сути, но они являются неотъемлемой частью соответствующих токенов предварительной обработки, а не отдельными. Стандарт говорит о них следующее:

A preprocessing number begins with a digit optionally preceded by a period (.) and may be followed by valid identifier characters and the character sequences e+, e-, E+, E-, p+, p-, P+, or P-.

Preprocessing number tokens lexically include all floating and integer constant tokens.

(С11 6.4.8/2-3; курсив добавлен)

По большей части препроцессор не обрабатывает токены предварительной обработки этого типа иначе, чем любые другие. Исключением являются управляющие выражения директив #if, которые оцениваются путем выполнения расширения макроса, замены идентификаторов на 0, а затем преобразование каждого токена предварительной обработки в токен перед оценкой результата по правилам C. Преобразование в токены учитывает суффиксы типов, что дает целочисленные константы добросовестный.

Однако это не обязательно дает результаты, идентичные тем, которые вы получили бы при вычислении тех же выражений во время выполнения, потому что

For the purposes of this token conversion and evaluation, all signed integer types and all unsigned integer types act as if they have the same representation as, respectively, the types intmax_t and uintmax_t.

(C2011, 6.10.1/4)

Вы продолжаете спрашивать

  1. Are there any dependencies or different behaviors with different environments, e.g. different compilers, C vs. C++, 32 bit vs. 64 bit machine, etc.? I.e., what does the preprocessor's behavior depend on?

Единственная прямая зависимость — это определения реализации intmax_t и uintmax_t. Они не связаны напрямую с выбором языка или архитектурой машины, хотя с ними может быть корреляции.

  1. Where is all that specified/documented?

Разумеется, в языковых спецификациях соответствующих языков. Я процитировал два наиболее важных раздела спецификации C11 и связал вас с последним проектом этого стандарта. (Текущий C — это C18, но он не изменился ни в одном из этих аспектов.)

Суффиксы целочисленных констант имеют значение для препроцессора, поскольку они влияют на подписание. -1u < 0 оценивается как false, но -1l < 0 оценивается как true.

Eric Postpischil 21.05.2019 20:54

Да, @EricPostpischil, они влияют на оценку управляющих выражений директив #if. Я, возможно, слишком тонко здесь разбиваю волосы, но хотя это имеет значение для C, я не считаю это значащим к препроцессору.

John Bollinger 21.05.2019 21:01
они влияют на оценку управляющих выражений #ifа такжеЯ не считаю это значимым для препроцессора кажутся неуместными.
ryyker 21.05.2019 21:19

Очевидно, что значения -1u < 0 и -1l < 0 в утверждении #if различны, поскольку они приводят к разным результатам, и поэтому выбор утверждений для включения различается. И разница только в суффиксе. Следовательно, суффикс имеет значение для предварительной обработки.

Eric Postpischil 21.05.2019 21:23

Как я уже сказал, я разделяю волосы довольно тонко. В частности, я помещаю вычисление выражения за рамки собственно препроцессора, поскольку (1) оно работает с жетоны, а не выполняет предварительную обработку токенов, и (2) оно регулируется обычными правилами C для вычисления, как в отличие от специальных правил, относящихся к препроцессору. Это соответствует вопросу, который выражает удивление по поводу синтаксиса C для целочисленных констант, которые распознаются и обрабатываются. Да, потому что никакое другое не определено для этой цели.

John Bollinger 21.05.2019 22:00

Хотя первое чтение сбивает с толку, я понимаю точку зрения Джона. Красиво структурированный ответ!

Michi 21.05.2019 23:03

Упрощенная версия TLDR:

l и ll являются эффективно (не буквально!) игнорируются условными выражениями препроцессора (по сути, все обрабатывается так, как если бы оно имело суффикс ll), однако u учитывается (обычно, как и для любой целочисленной константы C)!

Прочитав все замечательные ответы, я создал еще несколько примеров, которые показывают ожидаемое, но интересное поведение:

#include <stdio.h>

int main()
{
#if (1 - 2u > 0) // If one operand is unsigned, the result is unsigned.
                 // Usual implicit type conversion.
  printf("1 - 2u > 0\n");
#endif

#if (0 < 0xFFFFFFFFFFFFFFFF)
  printf("0 < 0xFFFFFFFFFFFFFFFF\n");
#endif

#if (-1 < 0)
  printf("-1 < 0\n");
#endif

#if (-1 < 0xFFFFFFFFFFFFFFFF)
  printf("-1 < 0xFFFFFFFFFFFFFFFF\n"); // nope
#elif (-1 > 0xFFFFFFFFFFFFFFFF)
  printf("-1 > 0xFFFFFFFFFFFFFFFF\n"); // nope, obviously
#endif

#if (-1 == 0xFFFFFFFFFFFFFFFF)
  printf("-1 == 0xFFFFFFFFFFFFFFFF (!!!)\n");
#endif
}

С этим выходом:

1 - 2u > 0
0 < 0xFFFFFFFFFFFFFFFF
-1 < 0
-1 == 0xFFFFFFFFFFFFFFFF (!!!)

Другие вопросы по теме