Уолтер Брайт великодушный пожизненный диктатор языка программирования D и основатель Digital Mars. За его плечами не один десяток лет опыта в разработке компиляторов и интерпретаторов для нескольких языков, в числе которых Zortech C++ первый нативный компилятор C++. Он также создатель игры Empire, послужившей основным источником вдохновения для Sid Meiers Civilization. Данная публикация первая в серии статей о режиме Better C в языке D.
Язык D был с самого начала спроектирован так, чтобы легко и напрямую обращаться к C и, в меньшей степени, C++. Благодаря этому в нём доступны бесчисленные C-библиотеки, стандартная библиотека C и конечно же системные API, которые как правило построены на API языка C.
Но C это не только библиотеки. На C написаны многие большие и неоценимо полезные программы, такие как операционная система Linux и значительная часть программ для неё. И хотя программы на D могут обращаться к библиотекам на C, обратное неверно. Программы на C не могут обращаться к коду на D. Невозможно (или по крайней мере очень сложно) скомпилировать несколько файлов на D и слинковать их в программу на C. Проблема в том, что скомпированные файлы на D могут обращаться к чему-то, что существует только в рантайме D, а добавлять его в линковку обычно оказывается непрактично (рантайм довольно объёмный).
Также код на языке D не может существовать в программе, если D
не контролирует функцию main()
, потому что именно так
происходит запуск рантайма D. Поэтому библиотеки на D оказываются
недоступны для программ на C, а программы-химеры (смесь C и D)
становятся непрактичными. Нельзя взять и просто попробовать язык D,
добавляя модули на D в существующие модули программы на C.
Так было до тех пор, пока не появился Better C.
Это всё уже было, идея не новая. Бьёрн Страуструп в 1988 году написал статью под названием A Better C. Его ранний компилятор C++ мог компилировать код на C почти без изменений, и можно было начать использовать возможности C++ тут и там, где это имело смысл не жертвуя существующими наработками на C. Это была блестящая стратегия, обеспечившая ранний успех C++.
Более современный пример Kotlin, в котором использовался другой подход. Синтаксически Kotlin не совместим с Java, однако он обеспечивает двустороннее взаимодействие с существующими Java-библиотеками, что позволяет постепенно мигрировать с Java на Kotlin. Kotlin действительно улучшенная Java, и его успех говорит за себя.
D как улучшенный C
D использует кардинально другой подход для создания улучшенного
C. Это не надстройка над C, не надмножество С, и он не тащит за
собой давние проблемы C (такие как препроцессор, переполнение
массивов и т.д.). Решение D это создание подмножества языка D, из
которого убраны возможности, требующие инициализирующего кода и
рантайма. И сводится оно к опции компилятору
-betterC
.
Остаётся ли урезанная версия D языком D? На это не так просто ответить, и на самом деле это лишь вопрос личных предпочтений. Основная часть языка остаётся на месте. Однозначно остаются все свойства, аналогичные C. В результате мы получаем язык, промежуточный между C и D.
Что убрано
Очевидно, что убран сборщик мусора, а вместе с ним возможности,
которые от него зависят. Память всё ещё можно выделять точно так
же, как и в C: при помощи malloc
или собственного
аллокатора.
Классы C++ и COMвсё ещё будут работать, но полиморфные классы D нет, поскольку они полагаются на сборщик мусора.
Исключения, typeid
, статические конструкторы и
деструкторы модулей, RAII и юниттесты убраны. Но возможно, мы
придумаем, как их вернуть.
На сегодняшний день в Better C уже стали доступны RAII и юниттесты. (прим. пер.)
Проверки assert
изменены, чтобы использовать
библиотеку C вместо рантайма D.
(Это неполный список, см. спецификацию Better C).
Что осталось
Гораздо более важно, что может предложить Better C по сравнению C?
Программистов на C в первую очередь могут заинтересовать безопасность доступа к памяти в виде проверок границ массивов, запрет на утечку указателей из области видимости и гарантированная инициализация локальных переменных. Далее следуют возможности, которые ожидаются от современного языка: модульность, перегрузка функций, конструкторы, методы, Юникод, вложенные функции, замыкания, выполнение функций на стадии компиляции (Compile Time Function Execution, CTFE), генератор документации, продвинутое метапрограммирование и проектирование через интроспекцию (Design by Introspection, DbI).
Генерируемый код
Возьмём следующую программу на С:
#include <stdio.h>int main(int argc, char** argv) { printf("hello world\n"); return 0;}
Она скомпилируется в:
_main:push EAXmov [ESP],offset FLAT:_DATAcall near ptr _printfxor EAX,EAXpop ECXret
Размер исполняемого файла 23068 байт.
Перенесём её на D:
import core.stdc.stdio;extern (C) int main(int argc, char** argv) { printf("hello world\n"); return 0;}
Размер исполняемого файла тот же самый: 23068 байт. Это неудивительно, потому что и компилятор C, и компилятор D генерируют один и тот же код, поскольку используют один и тот же генератор кода. (Эквивалентная программа на полноценном D занимала бы 194 Кб). Другими словами, вы ничего не платите за использование D вместо C при аналогичном коде.
Но Hello World это слишком просто. Возьмём что-то посложнее: пресловутый бенчмарк на основе решета Эратосфена:
#include <stdio.h>/* Eratosthenes Sieve prime number calculation. */#define true 1#define false 0#define size 8190#define sizepl 8191char flags[sizepl];int main() { int i, prime, k, count, iter; printf ("10 iterations\n"); for (iter = 1; iter <= 10; iter++) { count = 0; for (i = 0; i <= size; i++) flags[i] = true; for (i = 0; i <= size; i++) { if (flags[i]) { prime = i + i + 3; k = i + prime; while (k <= size) { flags[k] = false; k += prime; } count += 1; } } } printf ("\n%d primes", count); return 0;}
Перепишем на Better C:
import core.stdc.stdio;extern (C):__gshared bool[8191] flags;int main() { int count; printf("10 iterations\n"); foreach (iter; 1 .. 11) { count = 0; flags[] = true; foreach (i; 0 .. flags.length) { if (flags[i]) { const prime = i + i + 3; auto k = i + prime; while (k < flags.length) { flags[k] = false; k += prime; } count += 1; } } } printf("%d primes\n", count); return 0;}
Выглядит почти так же, но кое-что следует отметить:
- Приписка
extern(C)
означает использование соглашения о вызове из языка C. - D обычно хранит статические данные в локальном хранилище потока
(thread-local storage, TLS). C же хранит их в глобальном хранилище.
Аналогичного поведения мы достигаем при помощи
__gshared
. - Инструкция
foreach
более простой способ пройтись циклом по известному промежутку. - Использование
const
даёт знать читателю, чтоprime
никогда не изменится после инициализации. - Типы
iter
,i
,prime
иk
выводятся автоматически, уберегая от ошибок с непредвиденным приведением типов. - За количество элементов в
flags
отвечаетflags.length
, а не какая-то независимая переменная.
Последний пункт ведёт к скрытому, но очень важному преимуществу:
при обращении к массиву flags
происходит проверка его
границ. Больше никаких ошибок из-за выхода за границы массива! И
нам для этого даже не нужно было ничего делать.
И это только верхушка айсберга всех возможностей Better C,
которые позволят вам улучшить выразительность, читабельность и
безопасность ваших программ на C. К примеру, в D есть вложенные
функции, и по моему опыту, они практически не оставляют мне поводов
прибегать к запретной технике goto
.
От себя лично могу сказать, что с тех пор, как появилась опция
-betterC
, я начал переводить на D многие мои старые,
но всё ещё используемые программы по одной функции за раз. Работая
по одной функции и запуская набор тестов после каждого изменения я
постоянно сохраняю программу в рабочем состоянии. Если что-то
сломалось, мне нужно проверить только одну функцию, чтобы найти
причину. Мне не очень интересно дальше поддерживать свои программы
на C, и с появлением Better C для этого больше нет причин.