Когда говорят о языке C, чаще всего упоминают его долгую историю, мощь и простоту. Однако, несмотря на регулярные обновления стандарта (текущий — C23), есть целый ряд моментов, которые до сих пор почему-то остаются «за кадром». Статья «Obvious Things C Should Do» от Уолтера Брайта (Walter Bright) поднимает именно такие «странности» языка. Вы удивитесь, но решения всех проблем уже существуют — их воплотила реализация C внутри компилятора D (ImportC). Рассмотрим эти новшества подробнее и зададимся вопросом: почему же в стандарте C до сих пор нет таких улучшений?
🤔 Почему C не выполняет функции на этапе компиляции?
В классическом C нельзя вызывать обычные функции в enum-константах, даже если эти функции потенциально могут быть просчитаны на этапе компиляции. Например:
int sum(int a, int b) { return a + b; }
enum E {
A = 3,
B = 4,
C = sum(5, 6) // Ошибка в стандартном C
};
При компиляции GCC выдаст ошибку «не константное выражение». Хотя по сути это простейшая арифметика, которую любая оптимизация могла бы вычислить во время компиляции.
ImportC — реализация компиляции C внутри D-компилятора — исправляет это и позволяет реально «прогонять» функции на этапе компиляции (CTFE), при условии что они не делают I/O, не вызывают системные функции и не обращаются к изменяемым глобальным переменным.
Мнение автора: «С точки зрения современного программирования странно, что C так и не добавил эту возможность напрямую. Ведь это могло бы существенно упростить написание константных выражений и сделать код более безопасным и оптимизированным.»
🧪 Компиляция и юнит-тесты прямо в коде
Ещё одна «очевидная» проблема — редкие юнит-тесты в C. Традиционно для тестирования нужно собирать отдельный исполняемый файл, прописывать дополнительные цели (targets) в сборке. Многие разработчики пропускают тесты просто из-за сложности инфраструктуры.
Однако, если в C есть поддержка CTFE (Compile Time Function Evaluation), то можно писать тесты, проверяющие функции во время компиляции:
int sum(int a, int b) { return a + b; }
_Static_assert(sum(3, 4) == 7, "test #1");
В «обычном» GCC это тоже ошибка (не константное выражение), а в ImportC — нет. Юнит-тест выполняется прямо при сборке, и не нужно никаких «отдельных проектов». Автор упоминает, что сам активно пользуется этим подходом, потому что:
1. Нет дополнительных обвязок.
2. Проверка происходит каждый раз при компиляции.
3. Легко поддерживать всю «логическую» часть тестирования в одном месте.
🔍 «Каменный век» в языке: порядок объявлений
Одна из распространённых жалоб в C (и C++) — необходимость писать вперёд все прототипы функций. Например:
int floo(int a, char *s) { return dex(s, a); }
// Ой, ошибка, потому что compiler ещё не знает dex!
char dex(char *s, int i) { return s[i]; }
Чтобы исправить, нужно объявить dex заранее либо перенести floo после dex. Это ведёт к «перевёрнутому» порядку кода. С позиции современных языков такой подход выглядит архаичным: в большинстве из них можно вызывать функции в любом порядке, компилятор сам разберётся.
ImportC разрешает объявлять функции и переменные в любом порядке. Это кажется мелочью, но сколько человеко-часов уходит на «раскидывание» объявлений в .h-файлы и написание forward-деклараций!
📁 Борьба с заголовочными (.h) файлами
В C-философии принято держать интерфейсы в .h, а реализации в .c (или .cpp). Но если у вас три файла: floo.c, dex.h, dex.c, вы обязаны грамотно синхронизировать прототипы и определения. Любая рассинхронизация приводит к загадочным ошибкам на этапе линковки.
ImportC предлагает «импортировать» другой .c напрямую:
// floo.c
__import dex;
int floo(int a, char *s) { return dexx(s, a); }
И всё: не нужно писать .h, мучиться с соответствием сигнатур. Функция dexx из dex.c просто подхватывается, как если бы она была объявлена. Огромная экономия времени, особенно для крупных проектов, где поддерживать заголовки становится настоящей головной болью.
🤖 Личное впечатление и технические размышления
💡 Факт 1: Современные компиляторы уже умеют «жонглировать» порядком и выполнять часть кода во время компиляции. Так делают оптимизаторы (константная свёртка, inlining). Почему же C-стандарт официально не разрешает запуск обычных функций на этапе компиляции? Вероятно, из-за наследия совместимости с прошлым, а также опасений, что полный CTFE потребует введения дополнительных ограничений.
🤔 Мнение: Мне кажется, что ImportC показывает дорогу вперёд. Если бы стандартные комитеты C/C++ приняли часть этих улучшений, код стал бы проще и безопаснее. Но процесс стандартизации консервативен, поэтому остаётся надеяться на будущие релизы или пользоваться сторонними расширениями.
🛠️ Факт 2: В D (и потому в ImportC) используется идея, что типичные ограничения C (как порядок объявлений) — это пережиток. Вся лексическая структура, парсинг и т.д. делаются более гибкими, так что компилятор может «строить» внутреннее представление кода, независимо от порядка объявления. Технически это давно не проблема, ведь у нас есть мощные парсеры, а не простые одномоментные проходы.
🗒️ Итог
Язык C мог бы стать ещё удобнее и современнее, если бы в стандарт добавили:
• 🏹 Выполнение функций на этапе компиляции (CTFE).
• 🏹 Нативные compile-time юнит-тесты (через _Static_assert и расширенный функционал).
• 🏹 Гибкий порядок объявления глобальных переменных и функций (без forward-прототипов).
• 🏹 Импорт без header-файлов (как __import dex;).
Эти «очевидные» вещи давно доступны в реализациях вроде ImportC (компилятор D с «встроенным» C). К сожалению, «официальный» мир C не спешит меняться — видимо, историческая инерция и backward compatibility перевешивают пользу.
Если вы хотите опробовать эти улучшения уже сейчас, обратите внимание на ImportC и язык D. Вдруг окажется, что это спасёт вас от рутины с заголовками и морем forward-деклараций!
Ссылки на новость и дополнительные материалы
• Оригинальная статья: Obvious Things C Should Do