[Перевод] D как улучшенный C

?v=1

Уолтер Брайт — «великодушный пожизненный диктатор» языка программирования D и основатель Digital Mars. За его плечами не один десяток лет опыта в разработке компиляторов и интерпретаторов для нескольких языков, в числе которых Zortech C++ — первый нативный компилятор C++. Он также создатель игры Empire, послужившей основным источником вдохновения для Sid Meier«s 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 

int main(int argc, char** argv) {
    printf("hello world\n");
    return 0;
}

Она скомпилируется в:

_main:
push EAX
mov [ESP],offset FLAT:_DATA
call near ptr _printf
xor EAX,EAX
pop ECX
ret

Размер исполняемого файла — 23 068 байт.

Перенесём её на D:

import core.stdc.stdio;

extern (C) int main(int argc, char** argv) {
    printf("hello world\n");
    return 0;
}

Размер исполняемого файла тот же самый: 23 068 байт. Это неудивительно, потому что и компилятор C, и компилятор D генерируют один и тот же код, поскольку используют один и тот же генератор кода. (Эквивалентная программа на полноценном D занимала бы 194 Кб). Другими словами, вы ничего не платите за использование D вместо C при аналогичном коде.

Но Hello World — это слишком просто. Возьмём что-то посложнее: пресловутый бенчмарк на основе решета Эратосфена:

#include 

/* Eratosthenes Sieve prime number calculation. */

#define true    1
#define false   0
#define size    8190
#define sizepl  8191

char 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 для этого больше нет причин.

© Habrahabr.ru