Сравнение D и C++ и Rust на примерах

Данный пост основывается на Сравнение Rust и С++ на примерах и дополняет приведенные там примеры кодом на D с описанием различий.Все примеры были собраны с помощью компилятора DMD v2.065×86_64.

Проверка типов шаблона Шаблоны в Rust проверяются на корректность до их инстанцирования, поэтому есть чёткое разделение между ошибками в самом шаблоне (которых быть не должно, если Вы используете чужой/библиотечный шаблон) и в месте инстанцирования, где всё, что от Вас требуется — это удовлетворить требования к типу, описанные в шаблоне:

trait Sortable {} fn sort(array: &mut [T]) {} fn main () { sort (&mut [1,2,3]); } В D используется другой подход: на шаблоны, функции, структуры можно повесить guard, который не даст включить функцию в overload set, если шаблонный параметр не обладает определенным свойством.

import std.traits;

// auto sort (T)(T[] array) {} — версия без guard компилируется auto sort (T)(T[] array) if (isFloatingPoint! T) {}

void main () { sort ([1,2,3]); } Компилятор выразит недовольство следующим образом:

source/main.d (27): Error: template main.sort cannot deduce function from argument types!()(int[]), candidates are: source/main.d (23): main.sort (T)(T[] array) if (isFloatingPoint! T)

Однако получить почти идентичное «разрешающее» поведение Rust можно следующим образом:

template Sortable (T) { // допустим, мы можем отсортировать, если есть функция swap для этого типа enum Sortable = __traits (compiles, swap (T.init, T.init)); // В случае ошибки выведем понятное сообщение static assert (Sortable, «Sortable isn’t implemented for »~T.stringof~». swap function isn’t defined.»); }

auto sort (T)(T[] array) if (Sortable! T) {}

void main () { sort ([1,2,3]); } Вывод компилятора: source/main.d (41): Error: static assert «Sortable isn’t implemented for int. swap function isn’t defined.«source/main.d (44): instantiated from here: Sortable! intsource/main.d (48): instantiated from here: sort!()

Возможность выводить свои сообщения об ошибках позволяет почти во всех случаях избежать километровых логов компилятора о проблемах с шаблонами, но и цена такой свободы высока — приходится продумывать пределы применимости своих шаблонов и писать руками понятные (!) сообщения. С учетом того, что шаблонный параметр T может быть: типом, лямбдой, другим шаблоном (шаблоном шаблона и т.д., это позволяет имитировать depended types), выражением, списком выражений — зачастую обрабатывается только некоторое подмножество извращенных фантазий пользователя ошибок.

Обращение к удаленной памяти В D отсутствуют операторы освобождения памяти, максимум можно финализировать объект, чтобы освободить ресурсы когда надо программисту, а не GC. Но есть возможность выделять память через C-шное семейство функций malloc: import std.c.stdlib;

void main () { auto x = cast (int*)malloc (int.sizeof); // гарантированно освободим память при выходе из scope scope (exit) free (x); //, а теперь выстрелим себе в ногу free (x); *x = 0; } *** Error in `demo': double free or corruption (fasttop): 0×0000000001b02650 ***

D позволяет программировать на разных уровнях, вплоть до встраиваемого ассемблера. Отказываемся от GC — берем на себя ответственность за класс ошибок: утечки, обращения к удаленной памяти. Применение RAII (scope выражения в примере) может значительно сократить головную боль при таком подходе.

В недавно вышедшей книге D Cookbook есть главы, посвященные разработке кастомных массивов с ручным управлением памятью и написанию модуля ядра на D (без GC и без рантайма). Стандартная библиотека действительно становится практически бесполезной при полном отказе от рантайма и GC, но она была спроектирована изначально под использование их особенностей. Место embedded-style библиотеки все еще никем не занято.

Потерявшийся указатель на локальную переменную Версия Rust:

fn bar<'a>(p: &'a int) → &'a int { return p; } fn foo (n: int) → &int { bar (&n) } fn main () { let p1 = foo (1); let p2 = foo (2); println!(»{}, {}», *p1, *p2); } Аналог на D (практически повторяет пример на C++ из поста-источника):

import std.stdio;

int* bar (int* p) { return p; }

int* foo (int n) { return bar (&n); }

void main () { int* p1 = foo (1); int* p2 = foo (2); writeln (*p1,»,», *p2); } Вывод: 2,2

Rust в данном примере имеет преимущество, я не знаю ни один подобный язык, в который был встроен такой мощный анализатор времени жизни переменных. Единственное, что я могу сказать в защиту D, что в режиме safe компилятор предыдущий код не скомпилирует:

Error: cannot take address of parameter n in @ safe function foo

Также в 90% кода на D указатели не используются (низкий уровень — высокая ответственность), для большинства случаев подходит ref:

import std.stdio;

ref int bar (ref int p) { return p; }

ref int foo (int n) { return bar (n); }

void main () { auto p1 = foo (1); auto p2 = foo (2); writeln (p1,»,», p2); } Вывод: 1,2

Неинициированные переменные C++

#include int minval (int *A, int n) { int currmin; for (int i=0; i

import std.stdio;

int minval (int[] A) { int currmin = void; // undefined behavior foreach (a; A) if (a < currmin) currmin = a; return currmin; }

void main () { auto A = [1,2,3]; int min = minval (A); writeln (min); } Положительный момент: чтобы выстрелить в ногу нужно специально этого захотеть. Случайно неинициализовать переменную в D практически невозможно (может быть, copy-paste методом).

Более идиоматичный (и работающий) вариант этой функции выглядел бы так:

fn minval (A: &[int]) → int { A.iter ().fold (A[0], |u,&a| { if a

Для сравнения вариант на D:

int minval (int[] A) { return A.reduce! «a < b ? a : b"; // или //return A.reduce!((a,b) => a < b ? a : b); } Неявный конструктор копирования C++

struct A{ int *x; A (int v): x (new int (v)) {} ~A () {delete x;} };

int main () { A a (1), b=a; } Аналогичная версия на D:

struct A { int *x; this (int v) { x = new int; *x = v; } }

void main () { auto a = A (1); auto b = a; *b.x = 5; assert (*a.x == 1); // fails } В D структуры поддерживают только семантику копирования, а также не имеют механизма наследования (заменяется примесями), виртуальных функций и остальных особенностей объектов. Структура — просто кусок памяти, компилятор не добавляет ничего лишнего. Для корректной реализации примера необходимо определить postblit конструктор (почти конструктор копирования):

this (this) // в таком конструкторе есть доступ только к this { // доступа к структуре откуда копируем не имеем auto newx = new int; *newx = *x; x = newx; } Rust ничего за Вашей спиной делать не будет. Хотите автоматическую реализацию Eq или Clone? Просто добавьте свойство deriving к Вашей структуре:

#[deriving (Clone, Eq, Hash, PartialEq, PartialOrd, Ord, Show)] struct A{ x: Box } Аналога данного механизма в D нет. Для структур все подобные операции перегружаются через structual typing (часто путают с duck typing), если у структуры есть подходящий метод, то используется он, если нет, то реализация по умолчанию.Перекрытие области памяти #include struct X { int a, b; };

void swap_from (X& x, const X& y) { x.a = y.b; x.b = y.a; } int main () { X x = {1,2}; swap_from (x, x); printf (»%d,%d\n», x.a, x.b); } Выдаёт нам:

2,2

Аналогичный код на D, который тоже не работает:

struct X { int a, b; }

void swap_from (ref X x, const ref X y) { x.a = y.b; x.b = y.a; }

void main () { auto x = X (1,2); swap_from (x, x); writeln (x.a,»,», x.b); } Rust в этом случае однозначно побеждает. Я не нашел способа обнаружить memory overlapping на этапе компиляции на D.

Испорченный итератор В D абстракция итераторов заменена на Ranges, попробуем изменить контейнер при проходе: import std.stdio;

void main () { int[] v; v ~= 1; v ~= 2; foreach (val; v) { if (val < 5) { v ~= 5 - val; } } writeln(v); } Вывод: [1, 2, 4, 3]

При изменении массива range, полученный ранее не меняется, до конца блока foreach данный range будет указывать на данные «старого» массива. Можно заметить, что все изменения происходят в хвосте массива, можно усложнить пример и добавлять в начало и в конец одновременно:

import std.stdio; import std.container;

void main () { DList! int v; v.insert (1); v.insert (2); foreach (val; v[]) // оператор [] возвращает range { if (val < 5) { v.insertFront(5 - val); v.insertBack(5 - val); } } writeln(v[]); } Вывод: [3, 4, 1, 2, 4, 3]

В данном случае использовался двусвязный список из стандартной библиотеки. При использовании массива добавление в его начала всегда приводит к его пересозданию, но это не ломает алгоритм, старый range указывает на старый массив, а мы работаем с новыми копиями массива, а благодаря GC мы можем не беспокоиться о повисших в памяти огрызках. А в случае со списком не требуется перевыделения всей памяти, только под новые элементы.

Опасный Switch #include enum {RED, BLUE, GRAY, UNKNOWN} color = GRAY; int main () { int x; switch (color) { case GRAY: x=1; case RED: case BLUE: x=2; } printf (»%d», x); }

Выдаёт нам »2». В Rust жы Вы обязаны перечислить все варианты при сопоставлении с образцом. Кроме того, код автоматически не прыгает на следующий вариант, если не встретит break. В D перед switch может стоять ключевое слово final, тогда компилятор насильно заставит написать все варианты сопоставления. При отсутствии final обязательным условием является наличие default блока. Также в последних версиях компилятора неявное «проваливание» на следующую метку помечено как deprecated, необходим явный goto case. Пример:

import std.stdio;

enum Color {RED, BLUE, GRAY, UNKNOWN} Color color = Color.GRAY;

void main () { int x; final switch (color) { case Color.GRAY: x = 1; case Color.RED: case Color.BLUE: x = 2; } writeln (x); } Вывод компилятора: source/main.d (227): Error: enum member UNKNOWN not represented in final switchsource/main.d (229): Warning: switch case fallthrough — use 'goto case;' if intendedsource/main.d (229): Warning: switch case fallthrough — use 'goto case;' if intended

Случайная точка с запятой int main () { int pixels = 1; for (int j=0; j<5; j++); pixels++; } В Rust Вы обязаны заключать тела циклов и сравнений в фигурные скобки. Мелочь, конечно, но одим классом ошибок меньше.

В D компилятор выдаст предупреждение (по умолчанию предупреждения — ошибки) и предложит заменить; на {}.

Многопоточность #include #include #include

class Resource { int *value; public: Resource (): value (NULL) {} ~Resource () {delete value;} int *acquire () { if (! value) { value = new int (0); } return value; } };

void* function (void *param) { int *value = ((Resource*)param)→acquire (); printf («resource: %p\n», (void*)value); return value; }

int main () { Resource res; for (int i=0; i<5; ++i) { pthread_t pt; pthread_create(&pt, NULL, function, &res); } //sleep(10); printf("done\n"); } Порождает несколько ресурсов вместо одного:done

resource: 0×7f229c0008c0resource: 0×7f22840008c0resource: 0×7f228c0008c0resource: 0×7f22940008c0resource: 0×7f227c0008c0

В D аналогично Rust компилятор проверяет обращение к разделяемым ресурсам. По умолчанию вся память является неразделямой, каждый поток работает со своей копией окружения (которая хранится в TLS), а все разделяемые ресурсы помечаются ключевым словом shared. Попробуем записать на D:

import std.concurrency; import std.stdio;

class Resource { private int* value; int* acquire () { if (! value) { value = new int; } return value; } }

void foo (shared Resource res) { // Error: non-shared method main.Resource.acquire is not callable using a shared object writeln («resource », res.acquire); }

void main () { auto res = new shared Resource (); foreach (i; 0…5) { spawn (&foo, res); } writeln («done»); } Компилятор не увидел явной синхронизации и не дал скомпилировать код с потенциальной race condition. В D есть множество примитивов синхронизации, но для простоты рассмотрим Java-like монитор-мьютекс для объектов:

synchronized class Resource { private int* value; shared (int*) acquire () { if (! value) { value = new int; } return value; } } Вывод:

doneresource 7FDED3805FF0resource 7FDED3805FF0resource 7FDED3805FF0resource 7FDED3805FF0resource 7FDED3805FF0

При каждом вызове acquire, монитор объекта захватывается потоком и все остальные потоки блокируются до освобождения ресурса. Обратите внимание на возращаемый тип функции acquire, в D такие модификаторы как shared, const, immutable являются транзитивными, если ими отмечена ссылка на класс, то и все поля и возвращаемые указатели на поля также метятся модификатором.

Немного про небезопасный код В отличие от Rust весь код в D по умолчанию является @ system, т.е. небезопасным. Код, помеченный @ safe, ограничивает программиста и не дает играться с указателями, вставками ассемблера, небезопасными преобразованиями типов и прочими опасными возможностями. Для использования небезопасного кода в безопасном коде есть модификатор @ trusted, это ключевые места, которые должны быть тщательно покрыты тестами.Сравнивая с Rust, я очень желаю такую мощную систему анализа времени жизни ссылок для D. «Культурный» обмен между этими языками пойдет им только на пользу.

© Habrahabr.ru