Порівняння D і C + + і Rust на прикладах

    Даний пост грунтується на Порівняння Rust і С + + на прикладах і доповнює наведені там приклади кодом на D з описом відмінностей.
 
Всі приклади були зібрані за допомогою компілятора DMD v2.065 x86_64.
 
 

Перевірка типів шаблону

 
 
Шаблони в Rust перевіряються на коректність до їх інстанцірованія, тому є чіткий поділ між помилками в самому шаблоні (яких бути не повинно, якщо Ви використовуєте чужий / бібліотечний шаблон) і в місці інстанцірованія, де все, що від Вас потрібно — це задовольнити вимоги до типу, описані в шаблоні:
 
trait Sortable {}
fn sort<T: Sortable>(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! int
source / 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): 0x0000000001b02650 ***
 
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 <stdio.h>
int minval(int *A, int n) {
  int currmin;
  for (int i=0; i<n; i++)
    if (A[i] < currmin)
      currmin = A[i];
  return currmin;
}
int main() {
    int A[] = {1,2,3};
    int min = minval(A,3);
    printf("%d\n", min);
}

 
 
У D всі значення за замовчуванням ініцілізіруются значенням T.init, але є можливість вказати компілятору, що в конкретному випадку ініціалізація не потрібно:
 
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<u {a} else {u}
  })
}

 
 
Для порівняння варіант на 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<int>
}

 
Аналога даного механізму в D немає. Для структур всі подібні операції перевантажуються через structual typing (часто плутають з duck typing), якщо у структури є підходящий метод, то використовується він, якщо ні, то реалізація за замовчуванням.
 
 

Перекриття області пам'яті

 
#include <stdio.h>
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 <stdio.h>
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 switch
source / main.d (229): Warning: switch case fallthrough — use 'goto case;' if intended
source / 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 <stdio.h>
#include <pthread.h>
#include <unistd.h>

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: 0x7f229c0008c0
resource: 0x7f22840008c0
resource: 0x7f228c0008c0
resource: 0x7f22940008c0
resource: 0x7f227c0008c0
 
 
 
У 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;
    }
}

 
Висновок:
 
done
resource 7FDED3805FF0
resource 7FDED3805FF0
resource 7FDED3805FF0
resource 7FDED3805FF0
resource 7FDED3805FF0
 
При кожному виклику acquire, монітор об'єкта захоплюється потоком і всі інші потоки блокуються до звільнення ресурсу. Зверніть увагу на возращаться тип функції acquire, в D такі модифікатори як shared, const, immutable є транзитивними, якщо ними відзначена посилання на клас, то і всі поля і які повертаються покажчики на поля також метятся модифікатором.
 
 

Трохи про небезпечний код

На відміну від Rust весь код в D за умовчанням є @ system, тобто небезпечним. Код, що позначений @ safe, обмежує програміста і не дає гратися з покажчиками, вставками асемблера, небезпечними перетвореннями типів та іншими небезпечними можливостями. Для використання небезпечного коду в безпечному коді є модифікатор @ trusted, це ключові місця, які повинні бути ретельно покриті тестами.
 
Порівнюючи з Rust, я дуже бажаю таку потужну систему аналізу часу життя посилань для D. «Культурний» обмін між цими мовами піде їм тільки на користь.
    
Джерело: Хабрахабр
  • avatar
  • 0

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.