Сравнение 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, который сам выполняет подсчёт ссылок и удаляет ненужные объекты. Также в D есть разделение — освобождение ресурсов объекта и удаление объекта. В первом случае используется destroy(), во втором GC.free. Можно выделять память, управляемую GC — GC.malloc. Тогда программа сама освободит память во время запуска 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;
        }
    


    Есть чёткое разделение: если для объекта нужна семантика копирования — как у простых типов типа int — используются структуры. Если передача по ссылке, то используются классы. В книге Александреску (есть перевод) все эти моменты освещены.

    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. «Культурный» обмен между этими языками пойдет им только на пользу.

    Благодарю ReklatsMasters за дополнительный материал по GC и структурам.
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 50
    • +1
      А с производительностью что? Как-то можно сравнить?
      • +1
        Можно написать две программы, по одной из задач benchmarksgame.alioth.debian.org/. Я пытаюсь специализироваться в D, нужен еще спец. по Rust. Тогда можем и сравнить.
        • +1
          Реализации этих программ лежат в репозитории Rust и постоянно обновляются и тестируются (вот, к примеру, n-body). Я помню видел их своими глазами на BenchmarksGame, но сейчас там какие-то проблемы с лицензией выясняются, и их временно убрали с сайта. Языка D на этом сайте вообще почему-то нет. Если не боитесь установить (а лучше собрать) Rust, то всё в Ваших руках ;)
          • +3
            Rust (Исходник):
            $ rustc --version
            rustc 0.11.0-pre (6266f64 2014-06-06 23:06:35 -0700)
            host: x86_64-unknown-linux-gnu
            $ rustc nbody.rs --opt-level 3
            


            Все файлы, относящиеся к бенчмарку D лежат на гитхабе.

            DMD:
            $ dmd --help
            DMD64 D Compiler v2.065
            $ dmd -ofnbody source/nbody.d -release -inline -noboundscheck -O
            


            GDC (один из последних коммитов):
            $ gdc --version
            gdc (GCC) 4.8.2
            $ gdc -onbody source/nbody.d -O3 -frelease
            


            LDC2:
            $ ldc2 --version
            LDC - the LLVM D compiler (0.13.0-beta1):
              based on DMD v2.064 and LLVM 3.4.1
              Default target: x86_64-unknown-linux-gnu
            $ ldc2 -ofnbody source/nbody.d -release -O3
            


            Замерял следующим образом (далее таблица по 10 замеров для каждого билда):
            /usr/bin/time --verbose ./nbody 50000000
            


            Таблица замеров (время в секундах):


            Выводы: Rust и D примерно одинаковы по производительности. Для D лучше использовать GDC, когда важна скорость.

            Конфигурация машины:
            Intel® Core(TM)2 Quad CPU Q9400 @ 2.66GHz
            3.14.4-200.fc20.x86_64
    • 0
      Странно, ожидал увидеть много комментариев и обсуждений…
      • +3
        Сказывается малочисленность русскоязычного коммьюнити…
      • 0
        Господа, а можно сделать что-то вроде «Rust для программиста на PHP».
        Я понимаю, это выглядит смешно, но писать быстро и интуитивно понятно — это весьма важно.
        Просмотрев несколько статей про Rust не нашел там простого способа сделать ассоциативный массив.
        • 0
          Смотрите doc.rust-lang.org/std/container/trait.MutableMap.html
          Если вы имеете ввиду ассоциативный массив, который вроде и массив, и мапа, да еще и с какими угодно ключами, это своего рода фишка PHP, которая мало где еще есть, по крайней мере в стандартных библиотеках. И такой синтаксической поддержки нет нигде.
            • +1
              Забавно, что Вы спрашиваете об этом здесь, под постом о D :)
              Есть неплохой материал на это тему в Rust For Rubyists, однако я не вижу там ассоцииативных массивов… Про них неплохо написано в официальной документации HashMap. Что же касается полноценной статьи «Rust для программиста на PHP», то тут я, извините, пасс.
              • 0
                Ну, я в курсе что и Rust, и в D, нечто подобное есть. Вот тут как раз привели пример на D.
                Просто хотелось бы получить примеры простого кода как это делать правильно на Rust. А потом сравнить их и понять. Также не плохо было бы обсудить как эти массивы освобождаются из памяти и требуются ли для этого какие-то специальные действия. В PHP это все выглядит очень просто. А насколько просто это можно сделать в этих языках?
                • 0
                  В D ассоциативные массивы встроены в рантайм и полагаются на GC. Реализованы они как объекты, т.е. чтобы освободить память ассоциативного массива нужно обнулить все ссылки на него, а также можно вызывать метод clear для очистки внутренностей а.массива.
                  • +1
                    Я было попробовал то же самое сдалать на Rust, но тут же упёрся в тот факт, что числа с плавающей точкой не могут быть ключами деревьев (как TreeMap, так и HashMap). Длинная дискуссия тут, но основной смысл — что NaN не вписывается. В Rust пока консенсус таков, чтобы сделать обёркти для f32 и f64, которые реализуют способности Ord и Hash, так что могут быть и ключами.

                    Интересно, а как с этим (NaN) обходится D? В целом, судя по примеру, он сейчас намного более дружелюбен PHP разработчикам…
                    • +1
                      D REPL:
                      D> bool[double] map;
                      => map
                      D> map[0.0/0.0] = true;
                      D> map.keys
                      => [-nan]
                      D> map[-double.nan]
                      => true
                      


                      Для хеширования используется TypeInfo (TypeInfo для классов переопределяет поведение getHash), соответственно:
                      D> auto val = double.nan;
                      => val
                      D> typeid(double).getHash(&val)
                      => 360911136610280172
                      
                      • 0
                        Вот что сказал один из спецов по поводу использования float как ключей в D:
                        10:02 SiegeLord: So I checked, D associative arrays in the current release treat NaN's as equal, and in the master they treat them as unequal (i.e. they do the bad thing)
                        10:02 SiegeLord: The bad thing being, I think you can insert key NaNs, but not get them back
                        10:03 SiegeLord: So… essentially… that code example is nothing to be proud of
                        10:04 kvark: SiegeLord: so it works in 99.9% of cases, where we don't have a NaN key, and the rest of the cases are simply unable to get the key back? That sounds like a win to me
                        10:04 SiegeLord: It's a DoS vector
                        10:04 pczarn: why?
                        10:05 SiegeLord: You make insertion operation be O(n)
                        10:05 SiegeLord: Keep inserting NaN, and it keeps making the bucket longer and longer
                        10:05 SiegeLord: And it has to check them all by equality before determining that its a 'new' key

                        По его словам, решение в D не самое удачное, хотя я согласен, что это может быть удобно. Он утверждает, что бесконечно пихая некорректные числа в этот ассоциативный массив, каждая следующая операция будет линейно медленнее, что может стать орудием для атаки. Есть и такое мнение:
                        10:09 pyon Why would anyone want to compare floats for equality? This is a data type invented for scientific computations with inexact numbers. :-|

                        С этим трудно не согласиться. Может быть, поддержка float в таком контексте — излишество, которое может стать и проблемой безопасности (упомянутый DoS vector)?
                        • +1
                          Хм, я такого поведения в 2.065 не обнаружил (а если оно есть в ~master, то определенно нужен regression issue):
                          import std.stdio;
                          import std.datetime;
                          
                          void dos(bool[float] array)
                          {
                              array[double.nan] = true;
                          }
                          
                          void main()
                          {
                              bool[float] array;
                              auto test1 = benchmark!(() => dos(array))(100_000);   array.clear;
                              auto test2 = benchmark!(() => dos(array))(1000_000);  array.clear;
                              auto test3 = benchmark!(() => dos(array))(10000_000);
                              
                              writeln(test1[0].msecs / cast(double) 100_000);   // 0.00038
                              writeln(test2[0].msecs / cast(double) 1000_000);  // 0.000307
                              writeln(test3[0].msecs / cast(double) 10000_000); // 0.0002816
                          }
                          
                          • 0
                            Проверил на ~master (v2.066-devel-e3bada6) — тоже все нормально.

                            P.S. Грабли: array.clear; не очищает мапу, а вызывает alias для object.destroy. В 2.066 alias стал deprecated. Чтобы очистить мапу можно либо присвоить ей свежесозданную или пройтись по всем ключам и удалить их.
                      • 0
                        Ну, числа с плавающей точкой как ключи, я вообще-то ни разу не использовал. В доке по PHP написано, что они приводятся к целому ключу.
                        Но создание — это ещё относительно просто в обоих языках, а вот например вернуть из функции такой ассоциативный массив целиком — это уже похоже на нетривиальную задачу. Хотя может я зря поднимаю эту тему, и GC в обоих языках работает аккуратно и всегда? В PHP об этом вообще не думаешь. Оно просто работает:
                        <?php
                         function a($a) {
                          $a["add"]=1;
                          return $a;
                         }
                         var_dump(a(array("o"=>2)));
                         var_dump(a(array("c"=>4)));
                        ?>

                        array(2) { ["o"]=> int(2) ["add"]=> int(1) } array(2) { ["c"]=> int(4) ["add"]=> int(1) }
                        • 0
                          С этим как-раз таки проблем быть не должно. Пример без использования сборщика мусора:
                          use std::collections::hashmap::HashMap;
                          type MyMap = HashMap<String,int>;
                          
                          fn a(mut map: MyMap) -> MyMap {
                              map.insert("add".to_string(), 1);
                              map
                          }
                          
                          fn main() {
                              let mut x: MyMap = HashMap::new();
                              x.insert("o".to_string(), 2);
                              println!("{}", a(x));
                              x = HashMap::new();
                              x.insert("c".to_string(), 4);
                              println!("{}", a(x));
                          }
                          
                          • +2
                            Р-р-р.
                            За такой синтаксис надо бить по голове, учебником по… (эргономике?)
                            Зачем там .to_string()?
                            Почему insert(), а не []?
                            И вообще, квадратные скобки скорее соответствуют replace. Что будет, если там уже есть ключ add?
                            Про чистку памяти возникают еще интересные вопросы. Cначала просто: я правильно понимаю, что вот на этой строчке x = HashMap::new() память первого массива полностью освобождается? А в конце main освобождается и второй? А теперь хитрее: представим, что мы копируем из одного массива данные в другой. И ключи у них ну очень длинные, мегабайты длинной. Как будет работать такая процедура? Ключи будут дублироваться в памяти, или будет вестись счетчик ссылок на одинаковые строки и удалятся эти строки будут только когда их более никто не использует?
                            • 0
                              Ваше возмущение понятно. Строки и оператор индексирования — это одни из тех вещей, над которыми идёт работа сейчас, так что этот пример может измениться ближе к выпуску версии 1.0.

                              > Зачем там .to_string()?
                              Потому что голая строковая константа имеет тип &str, а ассоциативному массиву нужен String. Фактически to_string() выделяет память в куче и копирует туда строку. Не всегда нужно иметь строку в куче, поэтому по умолчанию Rust не делает ничего лишнего.

                              > Почему insert(), а не []?
                              Работа над этим ведётся.

                              > И вообще, квадратные скобки скорее соответствуют replace. Что будет, если там уже есть ключ add?
                              insert() замещает существующий элемент. Интерфейс MutableMap.

                              > я правильно понимаю, что вот на этой строчке x = HashMap::new() память первого массива полностью освобождается? А в конце main освобождается и второй?
                              Правильно.

                              > А теперь хитрее:…
                              В Rust никто за Вашей спиной не будет подсчитывать ссылки. Если Ваша программа копирует большие объекты, то можете использовать подсчёт ссылок для них, обернув в Rc:
                              use std::rc::Rc;
                              
                              fn foo(_x: Rc<String>) {}
                              fn main() {
                                  let x = Rc::new("my long string".to_string());
                                  foo(x.clone());
                              }
                              
                              • 0
                                Понятно. Альфа-версия. Ждем нормализации и фиксации синтаксиса.
                                Спасибо за ответы.
                            • +1
                              Иногда мне кажется что обычные плюсы сейчас намного читабельнее, сравним:
                              auto& a(const std::unordered_map<std::string, int>& arr)
                              {
                              	arr["add"] = 1;
                              	return arr;
                              }
                              
                              for(auto& el: a({{"o", 2}}))
                              	std::cout << el.first << " => " << el.second;
                              	
                              for(auto& el: a({{"c", 4}}))
                              	std::cout << el.first << " => " << el.second;
                              


                              почти дословно пхпшный вариант
                            • 0
                              В D ассоциативные массивы ведут себя как объекты:
                              import std.stdio;
                              
                              // Возврат из функции
                              int[string] add(int[string] array)
                              {
                                  array["add"] = 1;
                                  return array;
                              }
                              
                              // Измененение будет видно вне функции
                              void add2(int[string] array)
                              {
                                  array["add"] = 1;
                              }
                              
                              void main()
                              {
                                  writeln(add(["o": 2])); // ["add":1, "o":2]
                                  writeln(add(["c": 4])); // ["c":4, "add":1]
                                  
                                  // еще вариант записи
                                  writeln(["o": 2].add); // ["add":1, "o":2]
                                  // или даже так
                                  ["o": 2].add.writeln; // ["add":1, "o":2]
                                  
                                  // Демонстрация поведения by-reference
                                  int[string] arr = ["o": 2];
                                  add2(arr);
                                  writeln(arr); // ["add":1, "o":2]
                              }
                              

                              Проблем с GC быть не должно, если ссылок на ассоциативны массив больше нет, то он удаляется.
                              • 0
                                Мда, в D всё по-человечески.
                                Но вот разница by-reference — как то не совсем уловима. Вроде и в функции add массив-аргумент тоже правится?
                                • 0
                                  Если ассоциативные массивы копировались бы при передаче в функцию, то последний writeln вывел ["o":2], изменялся бы локальный ассоциативный массив внутри функции. Такое поведение имеют статические массивы:
                                  // статический массив
                                  void add(int[5] array)
                                  {
                                      // заполним весь массив значением 42
                                      array[] = 42; 
                                  }
                                  
                                  // динамический массив
                                  void add(int[] array)
                                  {
                                      // заполним весь массив значением 42
                                      array[] = 42; 
                                  }
                                  
                                  void main()
                                  {
                                      int[5] array = [1, 2, 3, 4, 5];
                                      add(array);
                                      writeln(array); // [1, 2, 3, 4, 5]
                                      add(array[]);
                                      writeln(array); // [42, 42, 42, 42, 42]
                                  }
                                  


                                  Если для статических массивов нужно ссылочное поведение, то к типу аргумента добавляют ref. Также важный момент add(array[]), через оператор [ ] (оператор для slicing) мы получаем динамический массив, который внутри ссылается на данные статического массива.
                                  • 0
                                    Не, я про то, что в вашем первом примере в функциях add и add2 аргумент передается одинаково: int[string] array
                                    и также он одинаково правится: array["add"] = 1;
                                    Разница только в возвращаемом значении.
                                    Значит, в обоих случаях массив передается по ссылке?
                                    • 0
                                      Так точно, в обоих случаях изменения будут видны вне функции, разницы нет. Я привел пример с add2 только для демонстрации тонкости того, что ассоциативные массивы всегда передаются по ссылке.
                                • 0
                                  Простите, я слоупок, случайно набрел на это обсуждение. У меня вопрос. Вот в этом месте
                                  add(["o": 2])
                                  

                                  получается, что объект создается в куче, а не на стеке? Это стандартное поведение или компилятор делает так потому, что ссылка на объект «утекает» из функции в качестве возвращаемого значения?
                                  • 0
                                    Стандартное поведение, ассоциативные массивы, реализованные в рантайме, всегда размещается в куче.
                                    • 0
                                      Да. Можно было не спрашивать. Когда-то давно интересовался D и для общего развития прочитал книгу Александреску и даже что-то для себя попробовал написать. Сейчас нашел книгу, посмотрел снова, действительно, память подо все объекты выделяется в куче и способа создать объект на стеке не существует. Это и сейчас верно или с тех пор что-то поменялось?
                                      • 0
                                        Сейчас можно создавать объекты на стеке с помощью scope, также используется библиотечное unsafe решение std.typecons.scoped.

                                        Книга Александреску немного устарела и многие новые фичи и изменения в языке там просто не отражены. Из свежих могу посоветовать D Cookbook.
                        • 0
                          Мне тоже нравится язык. Но я предпочитаю дождаться хотя бы первого релиза, потому что сейчас язык меняется каждый месяц, примеры, работающие год назад, сейчас уже не компилируются. Судя по слухам, что в ближайшие 2 года спецификация уже будет, то ждать осталось недолго.
                          • +2
                            В D есть такие ассоциативные мапы:
                            string[float] map0;
                            double[string][string] map1;
                            bool[bool][string][float] map2;
                            
                            map0[42.0] = "foo";
                            assert(42.0 in map0);
                            
                            map1["foo"]["bar"] = 42.0;
                            foreach(k1, submap; map1)
                                foreach(k2, val; submap)
                                    std.stdio.writeln(k1, " ", k2, " ", val);
                            
                            assert(map0.keys == [42.0]);
                            assert(map0.values == ["foo"]);
                            


                            Также можно в качестве ключей использовать свои типы, для этого нужно перегрузить операторы:
                            const hash_t toHash();
                            const bool opEquals(ref const KeyType s);
                            const int opCmp(ref const KeyType s);
                            


                            Полные доки: dlang.org/hash-map.html
                          • +2
                            Мне синтаксис D нравится больше всего, но использовать его в real-world проектах сложно: сырой компилятор (одни только невнятные ошибки чего стоят), обширная, но бесполезная документация, мало примеров кода. Жаль, что гугл с его административным ресурсом пиарит свой убогий Go (язык без классов и с обработкой ошибок по значению функции), а не интересный D =)
                            • +1
                              А можно вас попросить, пожалуйста, пример(ы) таких невнятных ошибок и ссылок на баги в компиляторе? Пока использую D по мелочи, один раз только в проекте до 1000 строк, и явных проблем не испытывал. Интересно просто, особенно после C++ на MSVC, с чем можно столкнуться.
                              • 0
                                Уже честно сказать не вспомню, дело было полгода назад. Выбирали язык, уж больно понравился D, но все-таки показался сыроватым. Там еще на этапе компиляции вылезали какие-то малоинформативные ошибки, мы побороли часть, потом поняли, что дальше так разработку вести нельзя — к сожалению или счастью, тот проект надо было делать быстро и нужен был «реальный результат» (как любит говорить один знакомый PM), а не игры с языками.
                              • 0
                                Уже использовал его в реальных проектах (сильно больше 1к строк), на серьезные баги не натыкался. Были проблемы с контрактами, они просто во многих случаях не вызывались, а должны были. Еще довольно жестокие грабли — __gshared модификатор, лучше его не использовать. Неаккуратное его использование может приводить к утечкам памяти и боли при синхронизации (ну это его назначение).
                                • 0
                                  Судя по Вашему сообщению, вы с низкоуровневыми языками на «ты». У нас все было не так радужно, были кодеры только на скриптовых языках, с некоторым Java-опытом — им было тяжко =)
                                • +1
                                  А мне нравится Go, мне нравится Plan9, я читал и правил для себя код Plan9 и компилятора Go и мне нравится как написан этот код, я на нем учусь. Мне по человечески симпатичны авторы этого кода. Я считаю идею классов никакой, я считаю концепцию исключений тупиковой. Я полагаю называть Go убогим скоропалительным.
                                  • 0
                                    Мне нравится, каким позитивным у Вас получился комментарий. По-больше бы добра в наши языковые войны ;)

                                    Я очень искренне хотел бы любить и Go, я согласен про классы (вопрос о том, хорошо ли неявное совпадение с интерфейсами, оставим на сладкое), и исключений не хочу касаться за милю. Убогим Go назвать ну никак нельзя. Объясните мне только, когда, наконец, можно будет самому написать такие же обобщённые контейнеры, которые предоставляет стандартная библиотека? Версия 1.2 уже вышла, что явно не располагает к масшабным изменениям в будущем.
                                    • 0
                                      Видимо никогда. Это решение Роба (командора) Пайка, которое не разделяет часть коммьюнити включая меня, но с которым мы будем мириться, потому что Пайку мы обязаны например unicode, редактором acme и серьезным куском Plan9 codebase.
                              • +1
                                Автор забыл описать мегафичу, которая есть в D — юниттесты. А также хочу дополнить эпизод про удаление.

                                > В D отсутствуют операторы освобождения памяти, максимум можно финализировать объект, чтобы освободить ресурсы когда надо программисту, а не GC.

                                В D как раз есть GC, который по-умолчанию сам выполняет подсчёт ссылок и удаляет ненужные объекты. Также в D есть разделение — освобождение ресурсов объекта и удаление объекта. В первом случае используется destroy(), во втором GC.free. Можно выделять память, управляемую GC — GC.malloc. Тогда программа сама освободит память во время запуска GC.

                                По поводу структур. Есть чёткое разделение: если для объекта нужна семантика копирования — как у простых типов типа int — используются структуры. Если передача по ссылке, то используются классы. В книге Александреску все эти моменты освещены.

                                LLIAMAH Например, в версии 2.065 я встречал некорректную обработку UTF-8 в debug mode, очень редко бывает. Пофиксили в 2.066.
                                • 0
                                  Спасибо, добавил в пост про GC и структуры. Встроенные юниттесты не влезли в оригинальную структуру статьи, добавлю чуть позже доп. пунктом.
                                • +2
                                  Может кому будет интересно сравнение Scala, C#, D и C dlang.ru/forum/218-subektivnoe-sravnenie-scala-csharp-d-i-c
                                  • +2
                                    я не знаю ни один подобный язык, в который был встроен такой мощный анализатор времени жизни переменных.
                                    Регионы появились Cyclone. Но успеха этот язык не имел.
                                    • 0
                                      int minval(int[] A)
                                      {
                                          return A.reduce!"a < b ? a : b";
                                          // или
                                          //return A.reduce!((a,b) => a < b ? a : b);
                                      }

                                      Лучше так:


                                      int minval(int[] A)
                                      {
                                          return A.reduce!q{ a < b ? a : b };
                                      }
                                      • 0
                                        #[deriving(Clone, Eq, Hash, PartialEq, PartialOrd, Ord, Show)]
                                        struct A{
                                            x: Box<int>
                                        }

                                        Аналога данного механизма в D нет.

                                        Есть же:


                                        struct A {
                                            mixin Clone!A;
                                            mixin Eq!A;
                                            mixin Hash!A;
                                            mixin PartialEq!A;
                                            mixin PartialOrd!A;
                                            mixin Ord!A;
                                            mixin Show!A;
                                            int x;
                                        }

                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.