Когда я узнал, что появился новый язык программирования системного уровня, с производительностью как у С++ и без сборщика мусора, я сразу заинтересовался. Мне нравится решать задачи с помощью языков со сборщиками мусора, вроде C# или JavaScript, но меня постоянно терзала мысль о сырой и грубой мощи С++. Но в С++ так много способов выстрелить себе в ногу и других хорошо известных проблем, что я обычно не решался.


Так что я влез в Rust. И, блин, влез глубоко.


Язык Rust все еще довольно молод, поэтому его экосистема пока находится в стадии начального развития. В некоторых случаях, например, в случае с вебсокетами или сериализацией есть хорошие и популярных решения. В других областях у Rust не все так хорошо. Одна из таких областей это OpenGL GUI, вроде CEGUI или nanogui. Я хотел помочь сообществу и языку, поэтому взялся за портирования nanogui на Rust, с кодом на чистом Rust, без связок с С/C++. Проект можно найти тут.


Обычно, знакомство с Rust начинается с борьбы с идеей borrow-checker. Как и у других программистов, у меня тоже был период, когда я не мог понять, как решить ту или иную проблему. К счастью, есть классное сообщество в #rust-beginners. Его обитатели помогали мне и отвечали на мои дурацкие вопросы. Мне понадобилось несколько недель на то, чтобы почувствовать себя более-менее комфортно в Rust.


Но я не подозревал, что когда сталкиваешься с проблемой, поиск решения похож на ориентацию в джунглях. Часто находится несколько ответов, которые похожи на решение твоей проблемы, но не подходят из-за крохотной детали.


Вот пример: представьте, что у вас есть базовый класс Widget, и вы хотите, чтобы у самих виджетов (Label, Button, Checkbox) были некоторые общие, легкодоступные функции. В языках вроде C++ или C# это легко. Нужно сделать абстрактный класс или базовый класс, в зависимости от языка, и наследовать свои классы от него.


public abstract class Widget {
    private Theme _theme { get; set; }
    private int _fontSize { get; set; }
    public int GetFontSize() {
        return (_fontSize < 0) ? _theme.GetStandardFontSize() : _fontSize;
    }
}

В Rust для этого нужно использовать типажи (traits). Однако, типаж ничего не знает о внутренней реализации. Типаж может определить абстрактную функцию, но у него нет доступа к внутренним полям.


trait Widget {
    fn font_size(&self) -> i32 {
        if self.font_size < 0 { //compiler error
            return self.theme.get_standard_font_size(); //compiler error
        } else {
            return self.font_size; //compiler error
        }
    }
}

» Запустить в интерактивной песочнице


Подумайте об этом. Моя первая реакция была "Эм, что?!". Конечно, существует справедливая критика ООП, но такое решение — это просто смешно.


К счастью, оказалось, что язык изменяется и улучшается с помощью Requests For Change, и этот процесс хорошо налажен. Я не единственный, кто считает, что такая реализация сильно ограничивает язык, и сейчас есть открытый RFC, призванный улучшить эту глупость. Но процесс идет с марта 2016. Концепция типажей уже много лет существует во многих языках. Сейчас — сентябрь 2016. Почему такая важная и необходимая часть языка все еще в плачевном состоянии?


В некоторых случаях можно обойти это ограничение, добавив функцию в типаж, которая реализована не в типаже, а в самом объекте, а потом использовать ее для обращения к реальной функции.


trait Widget {
    fn get_theme(&self) -> Theme;
    fn get_internal_font_size(&self) -> i32;
    fn get_actual_font_size(&self) -> i32 {
        if self.get_internal_font_size() < 0 {
            return self.get_theme().get_standard_font_size();
        } else {
            return self.get_internal_font_size();
        }
    }
}

» Запустить в интерактивной песочнице


Но теперь у вас есть публичная функция (функции типажа ведут себя как интерфейс, и сейчас нет возможности отметить функцию типажа как mod-only), которую все еще нужно реализовать во всех конкретных типах. Так что вы или не используете абстрактные функции и дублируете кучу кода, или используете подход выше и дублируете немного меньше, но все еще слишком много кода И получаете дырявый API. Оба исхода неприемлемы. И такого нет ни в одном из устоявшихся языков как C++, C# и, блин, даже в в Go есть нормальное решение.


Другой пример. В nanogui (в CEGUI такая концепция тоже используется) каждый виджет имеет указатель на родителя и вектор указателей на своих потомков. Как это реализуется в Rust? Есть несколько ответов:


  1. Использовать реализацию Vec<T>
  2. Использовать Vec<*mut T>
  3. Использовать Vec<Rc<RefCell<T>>>
  4. Использовать C bindings

Я попробовал способы 1, 2 и 3, в каждом нашлись минусы, которые сделали их использование неприемлемым. Сейчас я рассматриваю вариант 4, это мой последний шанс. Давайте взглянем на все варианты:


Вариант 1


Этот вариант выберет любой новичок Rust. Я так и сделал, и сразу столкнулся с проблемами с borrow checker. В этом варианте Widget должен быть владельцем (owner) своих потомков И родителя. Это невозможно, потому что родитель и потомок будут иметь циклические ссылки владения друг другом.


Вариант 2


Это был мой второй выбор. Его плюс в том, что он поход на стиль C++, использованный в nanogui. Есть несколько минусов, например, использование небезопасных блоков везде, внутри и снаружи библиотеки. К тому же, borrow checker не проверяет указатели на валидность. Но главный минус в том, что невозможно создать объект-счетчик. Я не имею ввиду эквивалент "умного указателя" из С++, или тип Rc из Rust. Я имею ввиду объект, который считает, сколько раз на него указывали, и удаляет сам себя когда счетчик достигает нуля. Вот пример на C++ из реализации nanogui.


Чтобы эта штука работала, нужно сказать компилятору, что удалять себя можно только изнутри объекта. Взгляните на пример:


struct WidgetObj {
    pub parent: Option<*mut WidgetObj>,
    pub font_size: i32
}

impl WidgetObj {
    fn new(font_size: i32) -> WidgetObj {
        WidgetObj {
            parent: None,
            font_size: font_size
        }
    }
}

impl Drop for WidgetObj {
    fn drop(&mut self) {
        println!("widget font_size {} dropped", self.font_size);
    }
}

fn main() {
    let mut w1 = WidgetObj::new(1);
    {
        let mut w2 = WidgetObj::new(2);
        w1.parent = Some(&mut w2);
    }

    unsafe { println!("parent font_size: {}", (*w1.parent.unwrap()).font_size) };
}

» Запустить в интерактивной песочнице


Вывод будет таким:


widget font_size 2 dropped
parent font_size: 2
widget font_size 1 dropped

Это нужно, чтобы не появилась ошибка use after free error, потому что память не обнуляется после удаления.


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


Ну, хорошо. Поступай как знаешь, Rust. Какой же способ реализации циклического направленного графа является идиоматическим в Rust?


Вариант 3


В итоге я нашел хорошую библиотеку для создания деревьев, которая называется rust-forest. Она дает возможность создавать узлы, указывать на узлы умными указателями и вставлять и удалять узлы. Однако, реализация не позволяет добавлять узлы разного типа T в один граф, и это важное требование библиотеки вроде nanogui.


Взгляните на этот интерактивный пример. Он немного длинноват, поэтому я не добавил полный листинг прямо в статью. Проблема в этой функции:


// Widget is a trait
// focused_widgets is a Vec<Rc<RefCell<Widget>>>
fn update_focus(&self, w: &Widget) {
    self.focused_widgets.clear();
    self.focused_widgets.push_child(w); // This will never work, we don't have the reference counted version of the widget here.
}

» Запустить в интерактивной песочнице


К слову, эту странную штуку можно обойти, но я все равно не понимаю, почему это вообще проблема.


let refObj = Rc::new(RefCell::new(WidgetObj::new(1)));
&refObj as &Rc<RefCell<Widget>>; // non-scalar cast

» Запустить в интерактивной песочнице


Заключение


Проблемы, с которыми я столкнулся при реализации способов 1, 2 и 3, наталкивают меня на мысль, что четвертый вариант со связкой с С — это единственный подходящий для моей задачи способ. И теперь я думаю — зачем делать связку с С, когда можно просто написать все на С? Или С++?


У языка программирования Rust есть положительные черты. Мне нравится, как работает Match. Мне нравится общая идея типажей, как и интерфейсов в Go. Мне нравится пакетный менеджер cargo. Но когда дело доходит до реализации деталей типажей, подсчета ссылок и невозможности переопределить поведение компилятора, я вынужден сказать «нет». Мне это не подходит.


Я искренне надеюсь, что люди продолжат улучшать Rust. Но я хочу писать игры. А не пытаться победить компилятор или писать RFC, чтобы сделать язык более подходящим моим задачам.


Примечание переводчика


Я не понял, что имеет ввиду автор, когда говорит «для корректной реализации такого счетчика нужно резервировать память глобально», как если бы это поведение было нетипичным для других языков, в частности С и С++. В них тоже нужно класть переменную в динамическую память если хочешь сохранить ее после завершения функции, верно?


К тому же, «нет простого способа указать компилятору не удалять переменную автоматически когда она выходит из области видимости» — похоже, просто неверное утверждение, потому что функция std::mem::forget создана специально для этого (из обсуждения на реддите).


Хорошие обсуждения статьи:


Поделиться с друзьями
-->

Комментарии (99)


  1. NeoCode
    14.09.2016 12:07
    -7

    Искренне не понимаю зачем, когда придумывают новый язык, первым делом берут и выкидывают что-то уже хорошо известное и отлаженное миллионами программистов за десятки лет. Вот чем их не устраивало классическое ООП в стиле «си с классами»? Я не против типажей rust и интерфейсов go, на самом деле это очень интересные и полезные фичи (и хорошо бы чтобы они появились в С++), но зачем при этом отказываться от старого и доброго? Почему нельзя просто добавить новое к существующему.
    Нет, конечно есть такие вещи которые лучше пересмотреть полностью и не повторять (например метапрограмминг на шаблонах в С++ — вместо него нужны нормальные синтаксические макросы), но самые простые основы, понятные всем и каждому, выкидывать все-же не стоит. Причем ломают-то именно «ради оригинальности» «чтобы было не как у всех», а не по какой-то осмысленной причине.


    1. snuk182
      14.09.2016 12:14
      +10

      Как минимум потому что Rust задумывался как замена низкоуровневому Си, а не ООП-подобным языкам. То, что на нем относительно безболезненно можно писать высокоуровневые вещи частично в ООП-стиле — просто невероятный подарок разработчиков.


      1. NeoCode
        14.09.2016 12:18
        +15

        Можно подумать что ООП это что-то такое супер высокоуровневое. Задач для ООП в системном программировании предостаточно, в том же ядре линукса множество вещей написано в ООП-стиле, пусть и на Си.


        1. snuk182
          14.09.2016 12:27
          +3

          Что совсем не значит, что их обязательно нужно так реализовывать. Многие ООП-решения подразумевают наличие нуллпоинтеров и свободного каста между типами, и потому в Rust в лоб не переносимы.


          1. DarkEld3r
            14.09.2016 12:35
            +3

            Многие ООП-решения подразумевают наличие нуллпоинтеров

            Например? Опять же, разве в расте есть какие-то проблемы с Option<&T>?


            1. snuk182
              14.09.2016 14:10

              Если возвращать опцию с ссылкой на неклонируемый тип, нужны хорошие скиллы в написании лайфтайм-зависимого кода. В обоих случаях (и по ссылке, и по значению) реализованное в лоб через Option выглядит как обезьяна в смокинге, чужеродно и неопрятно, и хочется переписать а для этого нужны скиллы, а если есть скиллы, то проще написать по новой без ООП-легаси


              1. DarkEld3r
                14.09.2016 14:19

                Ну с лайфтаймами — это общая "проблема" раста. (:
                Новая "непонятная" сущность, но осваивать её, если хочется применять язык, всё равно придётся.


                В остальном ладно, не буду спорить. Хотя всё равно не понял о каком именно "ООП коде" с нулевыми указателями речь.


                1. snuk182
                  14.09.2016 14:30
                  +2

                  Лично я сталкивался, когда пытался реализовать dependency injection container. Это не совсем системное программирование, но к ООП отношение имеет, и попило кровушки довольно.
                  Судя по тематическим вопросам на SO, народ на такое натыкается частенько. И ведь задает вопросы, и получает ответы, а не рыдает в днявки последними словами. Матчасть надо знать, вы правы 100%.


    1. DarkEld3r
      14.09.2016 12:19
      +11

      Бытует мнение, что агрегация лучше наследования. Хоть я и не очень люблю когда язык навязывает "единственный правильный способ", но плюсы у такого подхода имеются. Собственно, трейты раста могут сделать всё, что может "традиционное ООП", кроме наследования данных (не уверен, что это большая проблема). Наследование поведения есть.


      Ну и, как по мне, нужен простой способ сказать, что мы хотим делегировать реализацию такого-то трейта такому-то полю структуры.


      Касательно "почему нельзя добавить новое к существующему" — тогда рано или поздно мы получим не язык, а ужасного монстра. Сейчас и про С++ говорят, что знать его целиком невозможно. Насколько я понимаю, раст пытаются делать "простым", то есть не вводить 100500 вариантов синтаксического сахара для особых случаев и т.д. В принципе, такой подход мне по душе, хотя иногда грань между "простотой" и "примитивностью" довольно размыта.


      1. NeoCode
        14.09.2016 12:45
        +7

        C++ получится «монстром» не потому что в нем 100500 вариантов синтаксического сахара, а потому что огромная его часть (метапрограмминг на шаблонах) была «случайно открыта», а не «спроектирована». Соответственно мы получили то что я называю «все кишки наружу», когда код, который должен находиться внутри компилятора и не быть доступным программисту, оказывается в библиотеках типа Буста. Ну и отсюда неудобочитаемые километровые сообщения об ошибках в шаблонах и прочие странности. И еще вдобавок наследие Си: древняя и дремучая система include вместо нормальной модульности, и древний и дремучий лексический препроцессор вместо синтаксических макросов. Который когда-то был отдельной программой (это же так в стиле Unix-way) а затем намертво прирос к языку.
        Rust же задумывался как язык ограничений. При том что в нем сразу были продуманы и модульность, и синтаксические макросы, и функциональное программирование — тем ни менее было продумано и много ограничений. Не знаю, возможно кому-то это и нравится но мне не очень.


        1. DarkEld3r
          14.09.2016 12:54
          +3

          C++ получится «монстром» не потому что в нем 100500 вариантов синтаксического сахара, а потому что огромная его часть (метапрограмминг на шаблонах) была «случайно открыта», а не «спроектирована».

          Более-менее согласен, но дело не в этом. Новые стандарты, даже в среде плюсовиков, периодически вызывают реакцию в духе "как теперь можно знать весь язык?". И я это мнение вполне понимаю, хоть и воспринимаю нововведения как "новые возможности", а не "новые сложности". И да, есть и упрощения и дополнительные удобства, но объём растёт, как растёт и сложность. Особенно, если учитывать легаси, благодаря которому забывать про устаревшие вещи так сразу нельзя.


          Да, это путь любого живого языка, но я всё равно считаю, что подход "возьмём имеющийся язык и добавим туда чего-то" не идеален для создания новых. Выбрасывать тоже надо.


          Касательно сахара: это я говорил именно о расте. С++, пожалуй, не самый подходящий (контр)пример, лучше будет сравнить, например, со Swift, где такого сахара довольно много. Вроде как удобно, но в то же время, такое разнообразие вызывает опасения. Впрочем, я на Swift не пишу и далеко идущие выводы делать не буду, тем более, что язык активно развивается.


          Rust же задумывался как язык ограничений.

          Пусть так, но я это воспринимаю как наличие предохранителя на оружии, а не как связанные за спиной руки. Есть unsafe, да и макросами много чего можно наворотить.


          1. MacIn
            14.09.2016 17:16
            -1

            Более-менее согласен, но дело не в этом. Новые стандарты, даже в среде плюсовиков, периодически вызывают реакцию в духе «как теперь можно знать весь язык?».

            По-моему, дело именно в этом. Сразу оговорюсь — С++ не мой основной язык, я его использую очень редко и знаю весьма посредственно. Но когда возникает необходимость разбирать чей-то код, вымораживает не обилие конструкций и способов что-то сделать, а именно китайский язык шаблонов, который будучи применен «нехорошо», напрочь убивает читаемость.


            1. DarkEld3r
              14.09.2016 17:42
              +2

              У меня С++ как раз основной язык. Не скажу, что с лёгкостью разбираюсь в потрохах буста, но "в среднем" шаблоны проблем не вызывают. Опять же, их недостатки — это продолжение достоинств. Скажем, концепты дадут более внятные сообщения об ошибках, но они не буду обязательными, да и в каких-то (пусть и примитивных) случаях кода наоборот станет больше.


    1. torkve
      14.09.2016 12:33
      +17

      Как ни странно, это сделано не просто так (в расте вообще нет вещей, которые делались с мыслью «давайте не как в C++»).
      В расте нет наследования типов и есть наследование типажей, потому что первое в C++ приводит к различным проблемам с памятью (каждый программист на C++ в этом месте может и захочет сказать: «да надо просто знать несколько простых правил, а если ты их не знаешь, то и нечего программировать, это азы языка», но это плохая контраргументация против «нужно знать несколько простых правил обращения с типами в раст»).

      В расте это место намеренно упростили, в результате чего поверх структуры можно реализовывать любые интерфейсы, наследовать функциональность, но при этом: практически* любой объект можно удалить фактически простым free(), а скопировать — простым memcpy(). И не иметь при этом проблемы с памятью и головную боль с виртуальным наследованием, виртуальными деструкторами и вот этим вот всем.

      * конечно, для объектов типа fd/socket придётся реализовывать типаж Drop — аналог dispose в C# и close() в Java.

      Фактически это и есть си с классами, только лучше (вспомните, например, как си живёт со структурами типа sockaddr_in и sockaddr_in6 в стандартной библиотеке: они вообще никак не наследуются, но имеют обязательный список одинаковых первых полей, а в памяти кастуются к «базовому» классу).


      1. Antervis
        14.09.2016 14:13
        +2

        но при этом: практически* любой объект можно удалить фактически простым free(), а скопировать — простым memcpy(). И не иметь при этом проблемы с памятью и головную боль с виртуальным наследованием, виртуальными деструкторами и вот этим вот всем.

        А в с++ любой (правильно написанный) объект можно удалить простым delete, а скопировать — простым «a = b;». Проблемы с виртуальными функциями (да и всем остальным тоже) в с++ — от недопонимания. В другом яп будет другая реализация полиморфизма и точно так же найдутся люди которые не смогут её понять


        1. torkve
          14.09.2016 15:00
          +13

          Вот в этом уточнении: «правильно написанный» — вся соль :)
          Идея раста заключалась в том, чтобы не дать возможности случайно или по незнанию напороть ерунды в этом месте, неважно, понимаешь ты его полиморфизм или нет. Т.е. либо код не скомпилируется, либо какого-то проезда по памяти не будет.


        1. potan
          14.09.2016 18:24
          +4

          Не любой объект можно удалить delete, а только такой, класс которого в месте удаления точно известен компилятору или у него есть виртуальный деструктор.


          1. MacIn
            14.09.2016 18:44

            Логично, что виртуальный деструктор должен быть, если мы предполагаем передачу ссылок типа родительского объекта. Как иначе? Если у нас есть особая инициализация, то и особый деструктор потребуется. Если нет, то и стандартный справится.


            1. torkve
              15.09.2016 00:33
              +1

              Это ни капельки не логично, если задуматься. Более того, не знаю, что вы имеете в виду под «особой» инициализацией, но нам достаточно иметь в классе любое динамическое выделение памяти, чтобы получить отличный проезд:

              class A {
                  private:
                      int *x;
                  public:
                      A() : x(new int(1)) {}
                      ~A() { delete x; }
              };
              
              class B : public A {
                  private:
                      int *x;
                  public:
                      B() : x(new int(2)) {}
                      ~B() { delete x; }
              };
              
              int main() {
                  A *b = new B;
                  delete b;
                  return 0;
              }
              


              1. MacIn
                15.09.2016 01:20

                Это ни капельки не логично, если задуматься

                Более чем, если задуматься еще раз.

                Более того, не знаю, что вы имеете в виду под «особой» инициализацией, но нам достаточно иметь в классе любое динамическое выделение памяти, чтобы получить отличный проезд:

                Его и имею в виду. Делаешь инициализацию чего-то динамического — буфер, объект, что-то еще — имей деструктор.


                1. torkve
                  15.09.2016 11:28

                  Деструктор есть. Но он внезапно должен быть виртуальным, чтобы такого не происходило.


                  1. MacIn
                    15.09.2016 16:41

                    Почему внезапно? Это — логично. Делаешь динамическую инициализацию — позаботься о динамической деинициализации. Что здесь «такого»?


                    1. torkve
                      15.09.2016 17:59
                      +1

                      Давайте попробуем сначала :) Попробуйте представить, что вы зелёный новичок, а я попробую рассказать, как он может воспринимать это место в C++.
                      Мы знаем, что есть класс, у него есть поля и методы. Мы можем создавать новые объекты оператором new и удалять — оператором delete. Можно создать поле-объект в конструкторе и удалить в деструкторе.
                      Ещё мы знаем, что классы можно наследовать. При этом если мы конструируем класс-наследник, у него вызовется и конструктор базового класса, и конструктор наследника. Аналогично с деструктором, мы просто унаследовались, и вот уже разрушается и наследник, и базовый класс.
                      Конструкторы и деструкторы для нас при этом отделены от обычных функций: мы знаем, что они вызываются каскадом, а обычная унаследованная функция не будет звать функцию базового класса, если только в ней явно этот вызов не написать. И вот в этом месте таится наш подвох: мы знаем, что есть виртуальные функции, которые нужны, чтобы из базового класса звать реализацию наследника, но при этом и в голову не придёт, что то же самое может быть нужно для деструктора (тем более, что виртуальных конструкторов вообще не бывает), потому что он вроде как и так зовёт обе реализации.
                      Более того, конкретно в этом месте можно напороться ещё и потому, что общепринятая инструкция про виртуальные деструкторы ничего не говорит про динамическое выделение памяти, а учит, что виртуальный деструктор вам нужен, если вы пишете хотя бы одну чисто виртуальную функцию. А у нас таких и нет.


                      1. iCpu
                        16.09.2016 05:53

                        У всех языков есть свои недостатки и есть «не баги, а фичи». Виртуальные деструкторы из этой серии.
                        Вас бесит, что поведение обычных и виртуальных деструкторов отличается? Нас ещё на втором занятии в универе научили: «Если наследуешься от класса, у которого есть данные, делай его деструктор виртуальным. Короче, делай его виртуальным всегда.»
                        В Rust тоже есть бесящие концепции. Например, отсутствие переиспользования кода. Ага, та самая претензая про treats, ни тебе реализаций по умолчанию, ни полей. Например, сложности разрешения согласованности типажей, типов и реализаций. Ага, та самая, из-за которой добавили #[fundamental].
                        Давайте поговорим про них, так как виртуальные деструкторы уже изжили себя как проблема.


                        1. red75prim
                          16.09.2016 08:06
                          +1

                          Реализации по умолчанию в трейтах вполне себе есть. Использование полей легко реализуется наследованием от трейта, который даёт доступ к нужным полям. Пример


                          1. iCpu
                            16.09.2016 10:48
                            -1

                            Вы же сами понимаете, что не реализовали то, о чём я написал. Я даже не говорю о том, что вы выворачиваете внутреннюю структуру классов типажей-структур наружу, портя красивый пользовательский интерфейс.
                            Просто переопределите реализацию по умолчанию. Вы не сможете. У вас здесь даже нет «реализации по умолчанию», вам нужно явно указать «вот эту реализацию, пожалуйста». Каждый раз. Для каждого реализуемого класса. А если реализация разбита по частям…
                            Это фича языка, он так задуман, но вот эта вот проблема ни капли не сравнится с неудобством объявления одного единственного деструктора виртуальным.

                            «И вот эти люди запрещают нам ковыряться в носу» ©

                            Опять же, всё написанное выше справедливо для сравнения с плюсами, у чистых Сей нет даже такого инструментария, а любые поделки на тему ООП получаются монструозными вурдалаками с кастами указателей на указатели на функции между собой. Но ведь не я это затеял.


                            1. red75prim
                              16.09.2016 11:27
                              +3

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

                              Если библиотека на С++ разрешает наследование от класса, то это тоже выворачивает внутреннюю структуру класса наружу. Не вижу особой разницы. Доступ ко внутренностям реализации в Rust можно ограничить на уровне модуля.


                              Просто переопределите реализацию по умолчанию. Вы не сможете.

                              В смысле? Переопределенная реализация трейта, имеющего реализацию по умолчанию


                              вам нужно явно указать «вот эту реализацию, пожалуйста».

                              И? Несколько строк для указания какие поля нужно использовать и impl ThisTrait for MyShinyStruct {} вместо class MyShinyClass: public ThisBaseClass и кучи проблем с множественным наследованием.


                              Для каждого реализуемого класса. А если реализация разбита по частям…

                              По-моему, глубокие иерархии классов довольно давно считаются плохой идеей. Так что не вижу ничего страшного в том, чтобы для реализации плохих идей требовалось больше работать руками.


                              Насчет реализации разбитой по частям не понял.


                              но вот эта вот проблема ни капли не сравнится с неудобством объявления одного единственного деструктора виртуальным.

                              Эта "проблема" ни в коем случае не вызовет некорректного поведения программы, в отличии от. Не забывайте, что использовать С++ без статического анализатора опасно для ног.


                              1. iCpu
                                16.09.2016 12:43

                                Давайте я повторю одну мысль: сравнивать Rust и C++ не совсем корректно в силу различных подходов ко многим задачам и, соответственно, разным узлам концентрации костылей.

                                Эта «проблема» ни в коем случае не вызовет некорректного поведения программы, в отличии от.
                                Не забывайте, что мы говорим о разноуровневых проблемах. Сопоставимой проблемой будет, например, подключить не ту реализацию или потерять владение объектом. Ведь именно об этом весь сыр-бор — подключение не той стандартной реализации деструктора, разве нет?
                                По-моему, глубокие иерархии классов довольно давно считаются плохой идеей.
                                Несколько строк для указания какие поля нужно использовать и <...> вместо <...> кучи проблем с множественным наследованием.
                                Глубокие — это сколько? Я знаю, что на эту тему высказывают некоторые эксперты и «эксперты». Главное, что я слышал — «следуйте здравму смыслу».
                                У меня есть класс Сообщение, есть его потомки Входящее и Исходящее. У них есть базовые реализации Чтения и Записи. И есть под пару тысяч потомков-сообщений, каждое из которых использует и родительские поля, и родительские методы, в том числе, перегруженные. Да, их все можно переписать без ООП, но ИМХО
                                куда менее изящно. Структур будет больше, кода будет больше, а производительность — та же самая, потому что компилятор всё преобразует в код без ООП, а -O3 всё отлично упаковывает. Здравый смысл подсказывает, что оно мне не надо длинно и муторно при том же выхлопе.
                                В смысле? Переопределенная реализация трейта, имеющего реализацию по умолчанию
                                В смысле вы обязаны написать impl UsesFields for MyStruct {} В данном случае, это явный выбор определённой реализации, построенной только на интерфейсе, а не получение реализации по умолчанию, разве не так? К тому же, у вас нет доступа к базовой реализации после её переопределения. Вы просто получаете другую функцию на этапе компиляции и баста.

                                В Rust просто всё сделано иначе. И плюсовые подходы не работают. Я не понимаю, почему вы пытаетесь доказать обратное.


                                1. torkve
                                  16.09.2016 12:56

                                  Я тут мимо проходил, просто замечу, что реализация по умолчанию в расте делается один раз автором UsesFields как

                                  impl<T> UsesFields for T {}
                                  

                                  без дальнейших действий с точки зрения MyStruct. Частные специализации её просто перекроют, как шаблоны в C++.


                                  1. iCpu
                                    16.09.2016 13:00

                                    Разве он не попытается добавить реализацию всем от слова «Всем»?


                                    1. torkve
                                      16.09.2016 13:39
                                      +1

                                      Ну тут как. Вы можете наложить на T ограничения (типа для всех T, которые реализуют какие-то определённые типажи), и если вы в дефолтной реализации хотите использоввать какие-то свойства T, то очевидно, что это придётся делать. Но в общем и целом получается, что да, всем, тут полиморфизм в стиле шаблонов C++, если бы для них наконец допилили концепты.
                                      Я честно постарался и не смог придумать ситуацию, зачем мы можем не хотеть давать фичу всем классам, которые удовлетворяют требованиям этой фичи. По идее это плюс в любой непонятной ситуации, если мы не ограничиваем применение алгоритма конкретным типом.


                                  1. red75prim
                                    16.09.2016 13:42

                                    Точно, я как-то забыл про blanket имплементации. Только тут будет impl<T: Fields> UsesFields for T {}


                                1. red75prim
                                  16.09.2016 13:26
                                  +1

                                  У них есть базовые реализации Чтения и Записи [...]

                                  А, понятно, ООП используется для сериализации. Но обычно это удобнее делать специализированными средствами. Для Rust'а это — serde и ещё какие-то библиотеки.


                                  В Rust просто всё сделано иначе. И плюсовые подходы не работают. Я не понимаю, почему вы пытаетесь доказать обратное.

                                  Процитирую вас "В Rust тоже есть бесящие концепции. Например, отсутствие переиспользования кода."


                                  Я показал, что переиспользовать код в Rust'е вполне себе можно. Но теперь понятно, что вы имели в виду "Например, отсутствие переиспользования кода в привычной мне ООП манере." Против этого ничего не имею.


                                  1. iCpu
                                    16.09.2016 16:05

                                    Я показал, что переиспользовать код в Rust'е вполне себе можно. Но теперь понятно, что вы имели в виду «Например, отсутствие переиспользования кода в привычной мне ООП манере.» Против этого ничего не имею.
                                    Мне очень интересно, как вы сможете использовать несколько разных реализаций одного интерфейса. Ну, как на плюсах вызываются методы базового класса.


                                    1. red75prim
                                      16.09.2016 19:42

                                      Я уже говорил, трейты это — не совсем интерфейсы. Это тайпклассы. Вся сложная функциональность трейта строится на реализации нескольких функций. Например: трейт Iterator — чтобы реализовать всю функциональность этого трейта, достаточно определить для своей структуры метод fn next(&mut self) -> Item, остальные методы реализованы по умолчанию и определяются через него.


                                      Ну а как использовать методы базового класса? Это просто. Композиция вместо наследования: https://play.rust-lang.org/?gist=2729213fd2725d3e65ff801fea21240f&version=stable&backtrace=0


                                      1. iCpu
                                        17.09.2016 04:24

                                        Самое противное в вашем коде — он делает вид, что реализует то, что вы хотите мне показать. Но не реализует.
                                        Вы забыли одну тонкую вещь. Содержит != является. Вы не можете сделать

                                        struct threeBases{ b: Base, d: Base, dd: Base}
                                        impl threeBases{
                                        fn new() -> threeBases{
                                        threeBases{ b: Base{}, d: Derived{}, dd: DerivedDerived{} }
                                        }
                                        }

                                        И не можете хранить их всех в общем типизированном контейнере.
                                        И не можете дёрнуть из Base функциональность Derived. Ведь в вашем случае Base должен быть полностью определён. Да, он может знать трейт Derived, но тогда его нужно передавать явно одним из параметров или хранить слабую ссылку на Derived. И тут снова проблемы копирования и перемещения объектов вылезают, а ведь именно из-за них модель ООП и памяти «не такая, как в С++».
                                        А ещё перед вами встаёт дилемма. Если сделать содержимое Derived приватным, доступ к Base возможен только изнутри Derived, что не отвечает постановке задачи. Если же не делать, кишки вываливаются из пуза во всём своём зловонном великолепии. Тут, конечно, можно выкрутиться кодом вроде
                                        trait HasBaseStruct {
                                        fn base(&self)->Any;
                                        }
                                        

                                        Но это всё равно не будет полноценной заменой и потребует ручками писать немало кода для каждого класса и для любого вызова.
                                        Самая последняя проблема, у вас состояние отделено от поведения. И, по большому счёту, нет способа проверить, корректно или нет они сочленены. То есть, например, вместо наследования какой-нибудь, кхм, класс может просто создавать новый Base при каждом вызове этого метода. Это очень сложно проконтролировать, если вообще возможно.

                                        Вы создаёте код, который плодит проблемы. Плодит просто потому, что данная реализация языка не подходит для больших ООП задач. Не подходит совсем, без никаких «не привычных вам» способов. Это не хорошо и не плохо, это выбор авторов. К этому можно привыкнуть, но, уж извините, ряд задач потребует от вас генерации огромного объёма кода на том месте, где другие отделаются парой строчек. Это справедливо для всех языков.


                                        1. red75prim
                                          17.09.2016 08:05

                                          Мда. Повторяю, ООП — это не единственный способ бороться со сложностью. Нет ООП-задач, есть задачи, которые вы привыкли решать с помощью ООП.


                                          И не можете хранить их всех в общем типизированном контейнере.

                                          Очень даже могу.
                                          Почему там Box? По той же причине, по которой не получится поместить Derived в vector<Base> в С++.


                                          И не можете дёрнуть из Base функциональность Derived.

                                          Опять двадцать пять. Да, Rust не поддерживает смешивание интерфейсов и реализаций как в С++. Base и Derived в С++ это одновременно и интерфейсы и кучка данных, компилятор автоматически засовывает данные Base в Derived, что упрощает создание глубоких иерархий классов, которые страдают от хрупкости базового класса и других проблем специфичных для С++ и ООП.


                                          Да, он может знать трейт Derived, но тогда его нужно передавать явно одним из параметров или хранить слабую ссылку на Derived.

                                          Я уже показывал как это можно сделать. Не то чтобы это было очень нужно. https://habrahabr.ru/post/309968/#comment_9810600 для Derived добавить trait Derived: UsesFields + AccessToDerivedFields


                                          Если сделать содержимое Derived приватным, доступ к Base возможен только изнутри Derived

                                          Derived будет трейтом, унаследованным от Base, так что никаких "изнутри" не будет.


                                          Самая последняя проблема, у вас состояние отделено от поведения. И, по большому счёту, нет способа проверить, корректно или нет они сочленены.

                                          Что-что? В Rust я пишу реализацию интерфейса для конкретной структуры, все поля перед глазами, все интерфейсы реализуемые полями гарантированно не пересекают границы полей (если я вызываю метод поля, то я знаю, что другие поля не будут затронуты).


                                          Если я расширяю класс в С++, вызов методов базового класса может изменить что угодно в его полях, собранных в одну кучу благодаря наследованию.


                                          Вы создаёте код, который плодит проблемы. Плодит просто потому, что данная реализация языка не подходит для больших ООП задач. Не подходит совсем, без никаких «не привычных вам» способов.

                                          Пока я вижу только, что вы продолжаете натягивать сову на глобус, и это, естественно, вызывает проблемы. Нет ООП-задач. Есть задачи, которые вы привыкли решать с помощью ООП, до такой степени, что не видите других вариантов.


                                          Да, GUI — классическая задача, которую принято решать с помощью ООП, но это не значит, что не стоит пытаться решить её другим способом.


                                    1. red75prim
                                      16.09.2016 19:56

                                      Или вот так: https://play.rust-lang.org/?gist=db178ae263364d0839e6aded3de8f2b3&version=stable&backtrace=0


                                      Кстати, как это будет выглядеть в C++?


                                      1. roboter
                                        13.01.2017 10:29
                                        +1

                                        Поиграть как в статье написано и продать.


                                      1. Sirikid
                                        17.09.2016 01:49

                                        У меня получилось так: http://ideone.com/vPT5H2


                                        1. red75prim
                                          17.09.2016 07:02

                                          И теперь Says не имеет доступа к данным из Base. С чего собственно всё и начиналось. iCpu хотел иметь доступ из Rust'ового трейта к данным.


                                          1. Sirikid
                                            17.09.2016 07:33

                                            А что мешает описывать в трейтах приватные (доступные только трейту) методы? Тогда можно будет сделать приватные геттеры/сеттеры и ни у кого не будет бомбить «кишки наружу», а ещё лучше скрыть пару методов за абстракцией свойства (property).


                                            1. red75prim
                                              17.09.2016 08:27

                                              Мешает то, что такой возможности (пока?) нет https://github.com/rust-lang/rfcs/pull/52


                                              Ещё очень не помешала бы возможность делегировать реализацию трейта элементу структуры. https://github.com/rust-lang/rfcs/pull/1406


                                              Но с этим придётся подождать.


                                              1. Sirikid
                                                17.09.2016 08:45

                                                В смысле что мешает её добавить, оказывается уже и RFC есть. Делегация реализации это очень приятно, в Kotlin неплохо работает, но там не хватает псевдонимов типов.


                        1. Antervis
                          16.09.2016 08:51

                          а если в классе нет данных (чисто интерфейсный) то виртуальный деструктор тем более нужен


                          1. iCpu
                            16.09.2016 10:14
                            +1

                            Кроме тех случаев, когда класс предоставляет обёртку функций (тогда нужно посвятить автора в концепцию пространства имён).
                            Или когда он не имеет потомков (привет от шаблонов с их operator() и typedef using).
                            Или когда он является промежуточным классом в иерархии наследования.


                        1. torkve
                          16.09.2016 10:47
                          +3

                          Страуструп с вами, я не говорю, что виртуальный деструктор — это какая-то бага, что она меня бесит или что-то ещё в этом же духе.
                          Вся моя аргументация относится к исходному поинту дискуссии: в C++ есть потенциальные грабли с памятью при таком наследовании, неважно, неопытный/неумелый ты программист, или баг не отследили в процессе рефакторинг, важно, что этот проезд достаточно легко стриггерить. И система наследования в расте сделана так, чтобы этого проезда избежать, а не чтобы «не как в C++». Вот и всё.


                          1. iCpu
                            16.09.2016 11:22

                            Мы не можем сказать, что было мотивом именно такой реализации системы наследования в расте, и не думаю, что, к примеру, Хор через пару дюжин дней или лет признается: «да, мне тупо не нравилось наследование в плюсах, потому я и навалял что угодно, лишь бы не плюсовое [РосКомНадзор]», даже если изначально оно так и было.

                            Что до исходной аргументации, она так же не к месту. В Rust нет полноценного наследования. Есть реализация интерфейсов, то есть АОП, но не ООП. Сравнение плюсов с растам вообще не имеет смысла в этом контексте, ведь если наложить на плюсы те ограничения, которые действуют в расте, виртуальные деструкторы плюсам просто не потребуются. А если не накладывать, то расширение функциональности влечёт за собой и размножение грабель. Как и увеличение числа моделей работы с памятью в расте влечёт за собой проблемы новичков с потерей прав владения или неконтролируемое дублирование памяти по делу и без. Вот и всё.


                            1. ozkriff
                              16.09.2016 11:43
                              +2

                              Мы не можем сказать, что было мотивом именно такой реализации системы наследования в расте

                              А как же 100500 RFC и открытых обсуждений на эту тему?


                            1. torkve
                              16.09.2016 12:49
                              +1

                              > Мы не можем сказать, что было мотивом именно такой реализации системы наследования в расте, и не думаю, что, к примеру, Хор через пару дюжин дней или лет признается

                              Не, ну в голове-то он может думать что угодно, но процесс обсуждения и разработки в целом весь на гитхабе и на internals.rust-lang.org есть :) Оригинальное обсуждение дизайна я не нашёл, но вот, например, очень интересная старенькая дискуссия про добавление наследования структур. Оттуда ещё и можно ещё глубже по ссылкам сходить.

                              > Что до исходной аргументации, она так же не к месту. В Rust нет полноценного наследования. Есть реализация интерфейсов, то есть АОП, но не ООП. Сравнение плюсов с растам вообще не имеет смысла в этом контексте

                              Это, имхо, уже демагогия. Классическое наследование в ООП подразумевает наследование данных и поведения, в расте есть второе, но нет первого. Я слабо знаком с АОП и мне кажется, что оно вообще не про то, но готов поверить на слово. В любом случае, мы вынуждены сравнивать, просто потому что используем язык для решения задачи, а не потому что «там ООП». Даже исходный тред NeoCode начинается именно с жалобы, что ООП не такой.
                              Наложенные ограничения — это причина разницы, мы же работаем с последствиями.


                              1. iCpu
                                16.09.2016 13:17

                                Хорошо, я понял ваш посыл.

                                АОП действительно немного не про то, в нём объект строится из кусков, обладающих и поведением, и данными, а доступ к ним производится по запросу «обладает ли таким куском». В Rust же, по большому счёту, обыкновенный ООП, просто пропущенный через MVC-мясорубку, которая раскидала отдельно методы, отдельно данные и отдельно итерфейс. Я бы не сказал, что результат получился прям уж хорошим.

                                Что до "мы вынуждены сравнивать", давайте сравнивать всё по объективным параметрам. И в их числе должны быть не только «простота допущения ошибки», но и «простота её поиска\исправления», и «накладные расходы по её автоматическому выявлению».


                                1. red75prim
                                  16.09.2016 13:36
                                  +1

                                  Это не ООП, пропущенный через мясорубку. Мир на ООП не кончается. Если упрощенно, то это реализация typeclass'ов из Haskell'я для императивного языка.


                                  1. iCpu
                                    16.09.2016 15:41

                                    struct -> Model -> Модель данных
                                    trait -> View -> Пользовательский интерфейс
                                    impl -> Controller -> Внутренняя логика


                                1. torkve
                                  16.09.2016 13:48

                                  > И в их числе должны быть не только «простота допущения ошибки», но и «простота её поиска\исправления», и «накладные расходы по её автоматическому выявлению»…

                                  Это уже больше переход в обсуждение «чья реализация оказалась лучше», а не «почему так», я, пожалуй, в ней участвовать не буду :) Хорошо, что мы под конец друг друга поняли.


                      1. MacIn
                        16.09.2016 20:30

                        Попробуйте представить, что вы зелёный новичок, а я попробую рассказать, как он может воспринимать это место в C++.


                        Это уже не очень хорошее начало. Потому что виртуальный деструктор, о котором мы говорим, нам понадобится, если мы собираемся удалять объект по ссылке на его родительский тип, т.е. например если мы заносим его в коллекцию, которая контроллирует время жизни объекта. Например, это какие-то control'ы (виджеты, итп) и нам надо их занести в список «контролы окна», по которому мы будем потом проходить и в случае «чего» удалять их безотносительно внутренней реализации. Это уже не для новчика.

                        но при этом и в голову не придёт, что то же самое может быть нужно для деструктора

                        Странно; все книжки по «плюсам», которые я читал, всегда оговаривают последовательность вызова деструкторов, из которой ясно, как оно будет работать при вызове деструктора родительского класса.

                        Это, конечно, может быть проблемой — я, пожалуй, спишу на субъективность, как и то, что С++ — не мой основной язык, и не первый ООП язык для меня. Мне кажется, что достаточно понимать последовательность вызова д-в и придерживаться простого правила — не уверен, сделай д. вирутальным. Ресурсов это не сожрет.


                        1. torkve
                          16.09.2016 21:19
                          +1

                          > Это уже не для новчика.
                          Ну, не знаю, мне кажется, что новичок вполне может сразу гуй писать. Это студенту такое не дадут, а заставят до посинения писать алгоритмы сортировки, но студент != новичок.

                          > Это, конечно, может быть проблемой
                          Главная на самом деле проблема — это то, что такое знание энфорсится в программиста. Т.е. это правило написано в каких-то книжках, на любом форуме вам авторитетно объяснят, что вы идиот и должны сначала были читать учебник, но в лучших традициях C++ требование не прописано в стандарте, программисту разрешается стрелять в ногу «и так тоже», и вся надежда на добрую волю IDE и компилятора с их подсказками (которые программист конечно же не факт, что прочитает или не проигнорирует).

                          > Ресурсов это не сожрет.
                          Как же не сожрёт, когда сожрёт? :) И размер объекта вырастет:

                          0 ? compileOnce 'class A { public: ~A(){}}; class B { public: virtual ~B(){}}; std::cout << sizeof(A) << std::endl << sizeof(B) << std::endl;'
                          1
                          8

                          И производительность ухудшится (накладные расходы на походы в vtable). Другое дело, что важным это замедление станет ещё хрен знает когда, но факт есть :)
                          А ещё я видел библиотеку, где объявление реализации интерфейсного метода с virtual приводило к сегфолту! (т.е. в интерфейсе он был не virtual). Но это уже совсем другая история.


                          1. MacIn
                            17.09.2016 02:59

                            Ну, не знаю, мне кажется, что новичок вполне может сразу гуй писать. Это студенту такое не дадут, а заставят до посинения писать алгоритмы сортировки, но студент != новичок.

                            Вы под новичком, видимо, имеете в виду начинающего профессионального разработчика, я — изучающего язык. Тот, кто «уже не студент», тем более будет знать обсуждаемую особенность. Я думал, мы говорим о начинающих.

                            Как же не сожрёт, когда сожрёт? :) И размер объекта вырастет:
                            И производительность ухудшится (накладные расходы на походы в vtable


                            Хоспаде, очевидно же, что речь не про «с нулевыми в абсолютном выражении накладными расходами».


              1. iCpu
                15.09.2016 06:03

                И вот оно, непонимание! Поприветствуйте, дамы и господа!
                У вас в C++ коде есть использование динамической памяти в Си-стиле. Ваш код не позволяет определить, владеете ли вы объектом, на который ссылаетесь или нет, создан ли он, существует ли на момент удаления. То же самое можно сделать и в Rust.

                В плюсах уже не первый год для этого используются shared_ptr/weak_ptr/unique_ptr, и нормальный плюсовой код имел бы вид:
                class A {
                private:
                std::shared_ptr x;
                public:
                A(): x(new int(1)) {}
                };

                class B: public A {
                private:
                std::unique_ptr x;
                public:
                B(): x(new int(2)) {}
                };

                int main() {
                A *b = new B;
                delete b;
                return 0;
                }

                Upd: хабр сожрал шаблоны. Ненасытный.


                1. Antervis
                  15.09.2016 06:50
                  +3

                  Да, но ваш код отработает некорректно: B::x не будет освобождена: код. Единственный вариант получить то, что нужно, без виртуального деструктора, будет выглядеть так: код

                  п.с. для этого есть тэг вставки кода


                  1. iCpu
                    15.09.2016 06:56
                    +1

                    Действительно, спасибо за поправку.

                    п.с. они, тэги, для «отхабренных» не работают.


      1. DaylightIsBurning
        14.09.2016 15:33

        первое в C++ приводит к различным проблемам с памятью
        Вы не могли бы поподробнее объяснить, что имеется ввиду, для интересующихся неспециалистов в C++?


        1. Googolplex
          14.09.2016 16:00
          +4

          В первую очередь здесь, вероятно, имеется в виду object slicing. Но вроде бы там были и другие подводные камни.


        1. iCpu
          15.09.2016 06:18
          +3

          Есть две основные проблемы — копирование объектов при передаче их в качестве параметра и безконтрольное копирование указателей. Так же при наследовании очень часто забывают сделать деструктор базового класса виртуальным, из-за чего он не вызывается из кода потомков.
          1)
          class A{};
          int foo(A){};
          int main (){
          A a;
          return foo(A); // произойдёт лишнее копирование объекта
          }
          2) class A{
          int * a;
          A():A(new int(1);}
          ~A(){delete a;}
          void main(){
          A* a = new A;
          A b = *a;
          delete a; // b->a теперь удалён
          // ошибка памяти
          }

          Первые две проблемы на настоящий момент серьёзно подавлены. Третья же является не багом, но фичей, хотя и её держат в узде.
          1) С помощью ссылок и move-семантики.
          2) С помощью шаблонов 3) Практически все компиляторы сообщают, если базовый класс содержит поля и не содержит виртуальный деструктор.


    1. lair
      14.09.2016 12:36
      +3

      Почему нельзя просто добавить новое к существующему.

      Потому что будет нарушена концептуальная целостность.


    1. potan
      14.09.2016 18:20
      +3

      Новые языки обычно тем и интересны, что в них чего-то нет (как минимум они затрудняют что-то использовать), что мешает внедрению более привлекательных фич и способствует более простому/надежному программированию.


      1. NeoCode
        14.09.2016 19:33
        +1

        Мне новые языки обычно интересны тем что в них что-то есть :)



  1. DarkEld3r
    14.09.2016 12:13
    +10

    Фух, чуть не бросился критиковать не дочитав до примечаний — только тогда обратил внимание, что это перевод. Странно, конечно, что люди бросаются писать такие статьи основываясь на незнание, в общем-то, вполне базовых вещей. Это я о forget, например.


    Eсли честно, не понял и претензий к трейтам. Ну да, изнутри трейта нельзя обращаться к полям объекта. Зато можно это делать изнутри реализации трейта для конкретной структуры. Да и как иначе? Интерфейсы работают точно так же. Как по мне, не хватает разве что возможности удобным образом делегировать реализацию трейтов полям структуры — из-за этого приходится писать "мусорный" код. А в остальном вполне всё прилично.


  1. Disasm
    14.09.2016 12:19
    +11

    5 Хранить не список виджетов, а их уникальные идентификаторы. Ими можно хоть обкопироваться повсюду. Надо что-то сделать с самим виджетом — одолжил его, сделал, вернул.


  1. Gorthauer87
    14.09.2016 12:45
    +2

    Как по мне так самая большая проблема, так это то, что из функции нельзя вернуть тупо объект реализующий какой-то трейт, все жду, когда impl trait допилят.


    1. TargetSan
      14.09.2016 13:21
      +5

      Так на nightly уже


      1. Fedcomp
        14.09.2016 14:11

        Ну да давайте все сидеть на нестабильном nightly. Наверняка говорилось про стабильный билд.


        1. TargetSan
          14.09.2016 15:23
          +4

          Не полностью фразу написал, а поправить уже не успел. Имеется ввиду, что в nightly уже есть, так что до stable недолго. С учётом, что фича не делает глобальных изменений в коде, я надеюсь её вытащат из-под гейта без проволочек.


      1. snuk182
        14.09.2016 14:12

        Я правильно понимаю, это возможность вернуть реализацию типажа напрямую, без trait object?


        1. TargetSan
          14.09.2016 15:24
          +6

          Да
          fn iota(n: u32) -> impl Iterator<Item=u32>
          Значение неважно какого типа, поддерживающее указанный trait bound


          1. BasicWolf
            15.09.2016 10:09

            Уууу, круто! (Эмоции начинающего Rust'ника).


        1. Gorthauer87
          14.09.2016 15:25

          Да, чтобы не плодить бесконечные обертки.


      1. Gorthauer87
        14.09.2016 15:25

        Ну так там еще только ограниченый вариант.


        1. TargetSan
          14.09.2016 16:54

          А в чём там ограниченность? Действительно не в курсе. Я думал, что всё сводится к анонимизации исходного типа — так это уже вроде есть.


          1. farcaller
            15.09.2016 15:10
            +2

            Можно вернуть только один конкретный тип, factory-функции не выйдет.


  1. red75prim
    14.09.2016 13:53
    +8

    Вариант 6: прочитать документацию, и найти std::rc::Weak — слабую ссылку, вполне достаточную для организации интрузивного дерева виджетов с обратными ссылками на родителя (и на соседа, если захочется).


    Например, так: Запустить в интерактивной песочнице


    1. red75prim
      14.09.2016 16:58
      +3

      Я допустил ошибку. Неверно устанавливалась ссылка на родителя. Исправленный вариант: https://play.rust-lang.org/?gist=7b6e01d54da5fe79a920381c8539d370&version=stable&backtrace=0


      Приходится признать, что задача действительно не из лёгких, и исправление ошибки потребовало изменения структуры трейта.


  1. vintage
    14.09.2016 17:02
    -3

    Его бы рвение, да языку D, который по сути — по уму сделанный C++, но он похоже о нём даже не слышал :-(


    1. Bozaro
      15.09.2016 09:38
      +2

      В D, если я правильно понимаю, активно используется GC. И это ставит крест на многих областях применения, ради которых используют C/C++.


      1. vintage
        15.09.2016 10:03
        -1

        В стандартной библиотеке активно используется, да, хотя многие алгоритмы и не требуют gc. Сам язык имеет всё необходимое для работы без gc и есть альтернативные реализации стандартной библиотеки, не использующие gc. Например: https://github.com/Ingrater/phobos


        1. Bozaro
          15.09.2016 11:38
          +3

          А за счет чего в такой ситуации обеспечивается безопасность работы с указателями?


          1. vintage
            15.09.2016 12:07
            -2

            За счёт использования ссылок вместо указателей :-) Ну и заворачивания их в типы, обеспечивающие определённые ограничения. Например, счётчик ссылок: https://dlang.org/phobos/std_typecons.html#.RefCounted


  1. Sirikid
    14.09.2016 20:23
    +9

    Похоже автор попытался переписать с C++ прямо на Rust, вполне ожидаемо не получилось. /thread


  1. Amomum
    14.09.2016 20:41
    +4

    У меня тупой вопрос по вот этому куску кода:

    trait Widget {
        fn font_size(&self) -> i32 {
            if self.font_size < 0 { //compiler error
                return self.theme.get_standard_font_size(); //compiler error
            } else {
                return self.font_size; //compiler error
            }
        }
    }
    


    Разве в том же С++ базовый класс может дотянуться до полей в потомках? Или в каком-то другом популярном языке так можно?


    1. Sirikid
      14.09.2016 21:00

      В C++/C#/Java можно сделать абстрактный класс с приватными/защищенными полями, в Rust нет.


      1. Amomum
        15.09.2016 00:02
        +2

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


        1. Sirikid
          15.09.2016 00:41

          Это совсем для новичков/дурачков, типа «Попробуйте сами и получите вот здесь ошибку».


        1. ZeStas
          16.09.2016 10:11

          Как я понимаю, он больше удивляется как раз тому, что нет возможности как-то задать поля для типажей. Как следствие — сильно ограничена возможность писать дефолтные функции для самого типажа и приходится прописывать ее отдельно для каждого реализующего типаж класса.
          Разработчики же языка говорят, что это было сделано специально — требование типажа по наличию поля в реализующем классе может ограничить его применимость.


    1. AlexLeonov
      14.09.2016 23:22

      Если речь о полях класса (статические свойства), то да — в PHP такое сплошь и рядом, называется «позднее статическое связывание». Разумеется, это исключительно рантайм-фича (а как иначе?)

      Ну и трейты там же имеют право обращаться к любым свойствам и методам подмешивающего их класса-или-объекта. Если же свойства или метода вдруг нет — fatal error. Разумеется, тоже рантайм.

      Впрочем вы, очевидно, ждали ответ про compile-time. Такого, насколько я знаю, нет нигде. Буду рад, если кто-то укажет на пробел в моих знаниях.


      1. Amomum
        15.09.2016 00:13

        Пожалуй, единственное, что мне все же приходит в голову — это CRTP в C++. Но, по-моему, это слегка читерство:

        template< typename T >
        class Base
        {
        public:
        
            void foo()
            {
                std::cout << ((T *)(this))->text;
            }
        };
        
        class Derived : public Base< Derived >
        {
        public:
            Derived() : text("abc") {}
            std::string text;
        };
        
        int main(void)
        {
            Derived d;
            d.foo(); // выводит "abc"
        
            return 0;
        }
        


      1. vintage
        15.09.2016 00:42
        +1

        В D примеси имеют полный доступ к потрохам на этапе компиляции:


        /// Theme constants
        class Theme
        {
            int standardFontSize = 16;
        }
        
        /// Common widgets behaviour
        mixin template Widget()
        {
            /// current theme
            private Theme theme;
        
            /// Overrided font size
            private int currentFontSize;
        
            /// Constructor with injection of theme
            this( Theme theme )
            {
                this.theme = theme;
            }
        
            /// Return valid font size greater then 0
            public int fontSize()
            {
                if( this.currentFontSize <= 0 ) {
                    return this.theme.standardFontSize;
                } else {
                    return this.currentFontSize;
                }
            }
        }
        
        /// Buttons font size is independent of theme
        unittest {
            auto theme = new Theme;
            theme.standardFontSize = 100;
            auto button = new Button( theme );
            assert( button.fontSize != theme.standardFontSize );
            assert( button.fontSize == button.currentFontSize );
        }
        
        /// Button is clickable widget
        class Button
        {
            /// Add common behaviour
            mixin Widget;
        
            /// All buttons have 10px font size
            private int currentFontSize = 10;
        
            /// Print message on click
            public void click()
            {
                import std.stdio;
                writeln( "Clicked!" );
            }
        }


  1. orgkhnargh
    15.09.2016 10:25
    +1

    Просто нет простого способа указать компилятору не удалять переменную автоматически когда она выходит из области видимости.

    Щас не понятно было. Раз переменная выходит из области видимости, значит код ее напрямую увидеть не сможет, првильно? Тогда почему бы компилятору ее не удалить? Если же какое-то значение нужто сохранить на потом, надо его положить в какую-то структуру с соотвутствующе расставлеными lifetimes. Тогда значение будет доступно ровно столько, сколько надо. Ведь именно для этого и были придуманы lifetimes.


  1. redmanmale
    15.09.2016 12:26
    +1

    Всё понимаю, статья в основном про особенности языка, но мне интересны мотивы автора.


    Я хотел помочь сообществу и языку, поэтому взялся за портирования nanogui на Rust, с кодом на чистом Rust, без связок с С/C++.

    Я думал, что одна из важных фишек раста как раз в том, что в него можно быстро и просто интегрировать существующие библиотеки на С/С++, т.е. новую логику пишем на расте, используя годные, проверенные библиотеки на С/С++.
    Зачем просто портировать библиотеку с С++ на раст?


    1. DarkEld3r
      15.09.2016 13:50
      +2

      Во первых, напрямую можно использовать только сишные библиотеки, с С++ так не получится. Много ли языков, которые умеют плюсовые библиотеки использовать?..


      Во вторых, С-код хоть и можно напрямую звать, но это не особо удобно. Банальный пример: из библиотеки будут торчать функции типа create/free создающие "объекты", ну и функции по работе с ними. Если язык позволяет, то гораздо удобнее оформить это в виде класса с деструкторами, чтобы исключить возможность забыть об освобождении ресурса.


      Ну и в третьих, если писать обёртки к С++ либам, то их всё равно нужно собирать под каждую платформу. Обычно эти проблемы перекладываются на пользователей наших обёрток.