Добавить в корзинуПозвонить
Найти в Дзене
Цифровая Переплавка

Очевидные вещи, которые C до сих пор не делает — и как это исправляет ImportC

Когда говорят о языке 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 выдаст ошибку «не константное выражение». Хотя по сути это простейшая ариф

Когда говорят о языке 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