gcc и clang отступают от стандарта C: список расхождений с glibc и musl

Автор блога публикует неполный список несовместимостей между C, как он описан в стандарте, и C, каким он существует на практике для тех, кто работает с gcc/clang и glibc/musl. Имя автора в тексте не указано.

asm. По умолчанию gcc и clang воспринимают asm как ключевое слово-расширение. Это не вполне соответствует стандарту: в стандартном C asm остаётся допустимым незарезервированным идентификатором, поэтому код, соответствующий стандарту, может не скомпилироваться с настройками по умолчанию. Автор оговаривается, что для этого и существует флаг -std, но отмечает два момента: компиляторы по умолчанию не следуют стандарту, и непонятно, почему стандарт просто не сделает asm зарезервированным словом, как это сделано в C++.

FLT_ROUNDS. Стандарт требует, чтобы значение макроса FLT_ROUNDS из float.h динамически менялось вместе с режимом округления в среде выполнения. gcc же всегда определяет его как 1 (округление к ближайшему) независимо от текущего режима; в заголовке есть комментарий, что значение «должно меняться при вызовах fesetround». У clang для этого есть встроенная функция __builtin_flt_rounds(), у gcc такой нет.

Стандартные прагмы. Стандарт задаёт пять прагм для управления поведением вычислений с плавающей запятой: три добавлены в C99 (FP_CONTACT, FENV_ACCESS, CX_LIMITED_RANGE) и две в C23 (FENV_ROUND, FENV_DEC_ROUND). По словам автора, более чем через 27 лет после стандартизации C99 gcc не поддерживает ни одну из них, это просто пустые операции. Часть функциональности можно воспроизвести флагами командной строки, но они действуют на всю единицу трансляции, тогда как прагмы применимы к отдельным функциям и блокам. clang в основном поддерживает прагмы C99, но не прагмы C23. Автор обещает отдельный пост на эту тему когда-нибудь позже.

Квалификаторы в типе возвращаемого значения. По стандарту квалификаторы в типе возвращаемого значения функции отбрасываются, поэтому объявления int f(); и const int f(); должны быть эквивалентны. clang же считает квалификаторы значимыми и выдаёт ошибку conflicting types for 'f' (1 error generated).

Устаревший va_start в musl. Библиотека musl поставляет собственные версии некоторых заголовков, которые компилятор и так предоставляет сам, в том числе stdarg.h. Из-за этого компиляторы берут заголовок musl вместо встроенного, а он, по оценке автора, строго хуже: va_start определён как __builtin_va_start(v,l), что неверно, поскольку C23 позволяет опускать второй аргумент. Правильно было бы для STDC_VERSION >= 202311L использовать __builtin_c23_va_start(VA_ARGS). В итоге при сборке под musl нельзя применять одноаргументный va_start из C23, даже если компилятор его поддерживает.

В конце список ссылок «см. также»: заметки об обработке математических ошибок (math_errhandling) в glibc и musl, невозможность переносимо проверить ошибки преобразования строки в число в стандартном C, несоответствие gcc и clang стандарту C++ в части языковой компоновки функций и неточность одной man-страницы Linux.

Ключевые факты

  • gcc и clang по умолчанию трактуют asm как ключевое слово, хотя в стандартном C это допустимый незарезервированный идентификатор.
  • gcc всегда определяет FLT_ROUNDS как 1, хотя стандарт требует динамического значения; у clang есть __builtin_flt_rounds(), у gcc нет.
  • Из пяти стандартных прагм для арифметики с плавающей запятой (три из C99, две из C23) gcc не поддерживает ни одной, clang не поддерживает две из C23.
  • clang отвергает int f(); вместе с const int f() как конфликт типов, хотя по стандарту квалификаторы возвращаемого типа отбрасываются.
  • Заголовок stdarg.h в musl определяет va_start устаревшим образом, из-за чего одноаргументный va_start из C23 при сборке под musl недоступен.

Почему это важно

Gcc и clang, основные компиляторы C, а glibc и musl, основные стандартные библиотеки в этой связке. Заметка показывает, что «стандартный C» и то, что реально получается при сборке с настройками по умолчанию, не совпадают: часть расхождений касается даже базовых вещей вроде идентификатора asm и семантики стандартных прагм. Особенно заметна история с прагмами: по словам автора, gcc не реализовал их более 27 лет после C99.

Кому это важно

Авторам переносимого кода на C, разработчикам библиотек и встраиваемых систем, тем, кто собирает проекты под musl, а также тем, кто пишет численный код и опирается на управление режимами округления и контрактацией операций с плавающей запятой. Также полезно тем, кто следит за поддержкой C23 в компиляторах и библиотеках.

Как это применить

Сверьтесь со списком, если ваш код претендует на соответствие стандарту. Для поведения, зависящего от режима компиляции, автор напоминает о флаге -std. Если вы полагаетесь на стандартные прагмы с плавающей запятой, учтите, что в gcc они ничего не делают, а часть их эффекта можно получить флагами командной строки на уровне всей единицы трансляции. При сборке под musl не рассчитывайте на одноаргументный va_start из C23.

Можно ли доверять

Это личный технический блог с неполным списком, автор в тексте не назван, дата и версии протестированных компиляторов и библиотек не указаны. Примеры конкретные и проверяемые: приведены фрагменты заголовков и вывод clang. Реакции сопровождающих gcc, clang и musl в тексте нет. Оценки вроде «строго хуже» принадлежат автору заметки.

Риски и подводные камни

Код, где asm используется как обычный идентификатор, может не собраться с настройками по умолчанию. Код, зависящий от FLT_ROUNDS в gcc, всегда видит значение 1, даже если режим округления менялся во время выполнения. Прагмы стандарта в gcc принимаются, но не действуют, а флаги командной строки охватывают всю единицу трансляции, а не отдельные функции. Заметка описывает именно расхождения со стандартом, а конкретных расхождений, специфичных для glibc, в основном списке нет: она упоминается только во введении и в ссылке про math_errhandling.

«Две декларации должны быть одинаковыми, но clang считает их несовместимыми.»

— автор заметки «Your C is not standard C»