Pull to refresh

Comments 38

Верной дорогой идете, товарищи!!!
Сейчас я расскажу вам, что произойдет. Сегодня Rust — молодой, мочный язык, с n типами указателей, все потокобезопасно, дизайн языка from the ground up просто шикарен, не прикопаться. Он конечно же лучше старых злобных С++, которые ко всему прочему не всегда потокобезопасные, так еще и протекают!

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

И не мне, и не вам судить, что будет через н лет, мы не провидцы.

А если его никак не продвигать, то прогресса не будет, не будет ничего лучшего на замену плюсам.
Я не соглашусь, что сейчас он лучше, чем С++. Стандратная библиотека в плюсах дает фору растовской, гигантскую фору, тысячекилометровую фору. Да, в Rust есть классные штуки, но говорить, что Rust лучше плюсов — это глупости. Давайте я еще до#$усь до произношения «ява», потому что трясет. Кстати говоря, сравнивать его с Python/Java нет смысла, это языки из совершенно другой оперы и области применения.

И не мне, и не вам судить, что будет через н лет, мы не провидцы.

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

А если его никак не продвигать, то прогресса не будет, не будет ничего лучшего на замену плюсам.

Быть может, вместо того, чтобы заменять плюсы, можно просто их сделать лучше? Посмотрите на скачок, который мы сделали (ну, еще не до конца) с С++11 до С++17. Щас и корутины появятся и все-все-все.
Так чем вам Rust не угодил-то? Нравятся плюсы и хватает их — используйте и дальше, я вот использую и ничего, жив-здоров. Но давайте честно — как бы не улучшали С++ некоторые вещи в нём если и поменяются, то не в ближайшие 20 лет (т.к. ломать обратную совместимость не будут однозначно), а раз так, то есть незанятая ниша, которую может себе забрать Rust. Я вот пробую писать, не сказал бы что мне прям ВСЁ нравится, написание API поверх СИшных библиотек несколько затруднено, да и никакого хорошего примера пока нету, приходится самому пробовать все варианты и выбирать лучший, не есть гуд когда нет опыта в языке. Но некоторые вещи Rust для C++ недоступны, поэтому перекладывать опыт C++ на Rust несколько некорректно, скажем так, возможность управлять AST на этапе компиляции теоретически позволит исправить многие проблемы без изменения синтаксиса и некрасивых трюков.
По поводу стандартной библиотеки — вам не кажется что вы переоцениваете важность оной? Тот же Qt позволяет писать программы не прибегая к стандартной библиотеке вовсе(хоть и совместим с нею), помоему это признак того что сила плюсов в самом языке, а не библиотеке.
Кому нужна стандартная библиотека, когда есть Cargo:)
А можно конкретнее о том, где стандартная библиотека плюсов лучше растовской?
1. Специализация / динамическая диспетчеризация зависит только от того, ссылаюсь я в коде на тип «по ссылке» или «по значению»? Могу ли я попросить не плодить специализации для плоских типов, какого-нибудь i64, например, чтобы сохранить маленький размер бинарника, или, наоборот, попросить специализировать вызовы eq() для какого-нибудь развесистого типа, который я хочу хранить в Vec по ссылке, а не по значению?

2. Могу ли я сделать несколько реализаций типажа для одного и того же типа (в разных модулях), и, в зависимости от модуля, который я заимпортировал, получать по разному скомпилированный код?

3. Могу ли я делать реализации методов типажа по-умолчанию, опираясь на другие методы в том же типаже, как, например, в Хаскеле, или интерфейсах в Java 8?
1. Вид диспетчеризации зависит от использования типажа. Если типаж используется как баундинг на типовый параметр, то будет специализация и статическая диспетчеризация (это в расте называется «мономорфизация»), если используется «ссылка на типаж», то будет создан типаж-объект и будет динамическая диспетчеризация.

trait DoIt {
  fn do_it(&self);
}

fn do_it_statically<T: DoIt>(x: T) { // static dispatch
  // ...
}

fn do_it_dynamically(x: &DoIt) { // dynamic dispatch
  // ...
}


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

Надеюсь, я ответил на вопрос.

2. Не совсем понял вопрос, попробую ответить развёрнуто.

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

б) В пределах одного крейта, если имеется в виду один и тот же типаж для одного и того же типа, то может
быть только одна такая реализация, иначе будет конфликт реализаций: is.gd/S1FCmO.

в) Если трейт объявлен с одним и тем же именем и одними и теми же методами в разных модулях одного крейта,
то это де факте два разных типажа, и да, их можно оба реализовать для одного и того же типа, и подключать какой-то
из них выборочно: is.gd/QJWLkc.

Но оба таких типажа из разных модулей подключить просто так нельзя, т.к. будет конфликт имён.
Можно заюзать форму use modb::A as A1, но тогда всё равно нельзя будет использовать
метод типажа обычным образом, т.к. не ясно, от какого типажа метод использовать (error: multiple applicable methods in scope).
Надо будет вызывать метод через UFCS: is.gd/LTfXcZ

3. Да, конечно можете.
1. Я имел ввиду не это.

В расте тип &ClickCallback или Box<ClickCallback> называется «объект-типаж» и включает в себя указатель на экземпляр типа T, который реализует заданный типаж (ClickCallback), и указатель на таблицу виртуальных методов.


Могу я ссылаться на экземпляр, при статическом вызове методов (не через таблицу)? Могу я не ссылаться на тип (то есть работать с ним «по значению»), но вызывать методы через таблицу?

2. Я не очень понял, конкретно вопрос такой: вот, допустим, есть тип HashMap (стандартный, не в моем крейте). Он хочет, чтобы тип ключа имел реализацию типажа Hash (и Eq). Есть мой тип MyKey. Могу ли я в одном модуле написать impl Hash for MyKey {foo foo foo}, в другом — impl Hash for MyKey {bar bar bar}. Потом, когда я пишу: let map = HashMap<MyKey, i64>(); — компиляция бы зависела от того, какой модуль с реализацией impl Hash for MyKey я заимпортил?
2. Внутри одного контейнера (crate) — нельзя, в разных — можно, но придётся импортировать типажи под разными и именами и вызывать не как методы, а как функции.
Так а если HashMap уже сам вызывает их определенным образом? Не я типаж использую, а HashMap.
Так откуда HashMap узнает какую реализацию Hash использовать на MyKey, если таких реализаций несколько?
В общем, нет, нельзя.
Узнает по тому, какую реализацию импортировали. Если импортировали обе — пусть будет конфликт, ошибка компиляции.
Единица компиляции в расте — крейт, а не модуль. В пределах крейта для пары типаж—тип может быть только одна реализация.
Ну а так как если я объявлю MyKey в одном крейте, в другом и Hash, и MyKey окажутся «внешними», поэтому определить другую реализацию я тоже не смогу. Ясно-понятно.
В общем, правило такое: чтобы реализовать типаж для типа в контейнере, этот контейнер должен определять либо тип, либо типаж. Если и то, и другое для него внешнее, то определить реализацию нельзя.
Да, я это уже понял, то же самое написал комментарием выше. Просто подытожил, что абстрагирование определенного плана не поддерживается.
1. При динамической диспетчеризации и идёт ссылка на экземпляр в самом методе, &self и есть такая ссылка.
При статической диспетчеризации никаких «таблиц» нет в принципе. Я не понимаю вопроса. Варианта два: либо есть таблица виртуальных методов, и в вызванный метод отдаётся ссылка на экземпляр типа через &self, либо нет таблицы, есть статический вызов метода, но в него всё равно передаётся та же самая ссылка на экземпляр типа &self. Всё происходит прозрачно.

Как ни вызывай, в &self будет ссылка на конкретный экземпляр типа, на котором вызывается метод.

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

2. Нет, нельзя. Два раза один и тот же типаж для одного и того же типа реализовать нельзя, иначе непонятно, что к чему относится.
Я совсем запутался с этими обозначениями Раста. Если я хочу динамический массив (Vec в Расте, vector в C++), который хранит ссылки на стуктуры/объекты/типы (называйте как хотите), которые лежат где-то еще (например, в куче, или аллоцированы с помощью malloc). При этом я хочу реализовать что-то типа vec.contains(elem), который бы проходился по всем элементам в векторе и вызывал их eq(). Я могу добиться того, чтобы в этом tight loop в скомпилированном коде не проверялась таблица виртуальных методов на каждой итерации? То есть хочу ссылки, но не «жирные», а «тонкие». Массив ссылок, но не абы на что, а на статически известный один и тот же тип.

CONSTantius
Вам типаж для этого вообще не нужен. Vec статически диспетчеризуется на прямой вызов .eq() у T.
Да, всё верно. Пользуйтесь Vec<&MyType> и будьте счастливы!
Как не нужен. eq() это же часть типажа Eq.

Почему статически. В статье Vec<Box<ClickCallback>> динамически.
Типаж нужен, чтобы хранить в векторе структуры *разных* типов, все из которых реализуют этот типаж. Гипотетически, Vec<&Eq> — это вектор ссылок на структуры, все из которых умеют сравниваться, но имеют разные типы. На практике что там с чем может сравниться — непонятно, поэтому такой код может и не скомпилируется.
Проверил. Типаж Eq не является объекто-безопасным (насколько я понимаю, как раз потому, что это бред) — не компилируется это всё.
То есть тип диспетчеризации определяется при инстанцировании типа. То есть Vec<&MyType>(10) будет занимать, скажем, какой-то заголовок + 40 байт (если ссылка 4-байтовая), а Vec<&ClickCallback>(10) — заголовок + 80 байт, если ссылка на таблицу виртуальных методов тоже 4-байтовая. Правильно?
1. Нет, потому что, например, только &ClickCallback содержит указатель на таблицу (это жирный указатель). Насколько я понимаю, ClickCallback как типаж-значение во время исполнения вообще никак не представляется.

А зачем это может понадобиться?
Можно ли на типажах реализовать open-multi methods / dynamic dispatch?
Я имею ввиду, можно ли сделать перегрузку по аргументу функции типа такого:
class Base {};
class Der1:public Base{};
class Der2:public Base{};
void print(Der1* d1) {cout<<"Der1";}
void print(Der2* d2) {cout<<"Der2";}
void main() {
  vector<Base*> vec;
  vec.push_back(new Der1);
  vec.push_back(new Der2);
  for(Base* el:vec) {
    print(el);
  }
}

В C++ это не работает, хотя Stroustrup давно ратует за внедрение open multi-methods.
Перегрузки функций в стиле С++ в Rust вообще нет, насколько мне известно. То есть, вот такой код не скомпилируется:
fn foo(a: u32) {
...
}
fn foo(a: &str) {
...
}

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

А в чём проблема использовать для подобных случаев виртуальные методы?
Нужны open multi-methods для того, что бы создавать гибкие, лаконичные и хорошо поддерживаемые архитектуры. Для избежания костылей, короче говоря. Правило Гринспина «Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, bug-ridden, slow implementation of half of Common Lisp» в том числе об отсутствии открытых мульти-методов. Этот механизм помогает избегать нарушения инкапсуляции, например. В некоторых случаях позволяет сохранять локальность изменений, когда новая функциональность не требует изменений в «базовом» коде и т.п. Все эти преимущества часто рассматриваются в контексте double/multi dispatch

Представленный код не требует RTTI, поддержка open multi-methods может быть даже эффективней по производительности, чем патерн Visitor, доказанно Страустуропом, также в этой статье детально описываются преимущества этого паттерна.
Для C++ существует несколько библиотек реализующих эту функциональность, существуют даже специальные препроцессоры с поддержкой модифицированного синтаксиса.

Я знаю только один способ реализовать мульти-методы через динамический полиморфизм — патерн visior. На мой взгляд это решение никудышное т.к. требует нарушения инкапсуляции, а также для добавления нового метода необходимо менять код в трех разных местах — костыль ужасный на мой взгляд. Очень легко допустить ошибку при его использовании. Поэтому я стал пользоваться библиотекой которая реализует мультиметоды для c++11 с производительностью близкой к вызовам виртуальных функций — yomm11.

Я вышел на эту идею не в процессе изучения теории программирования или прочтения книг, а через практику (см. пример asteroid). Стал гуглить, как умные люди решают проблему и вышел на yomm11, что, на мой взгляд, лучшее доказательство необходимости поддержки multimethods в С++ — практическая потребность.
Понял. Подобную функциональность трейты обеспечивают. Причём объектам, которые вы кладёте в вектор, не обязательно даже быть одного типа. Получается, возможно, не так лаконично, но делается так:
trait Foo {
  fn bar(&self);
}

impl Foo for u32 {
  fn bar(&self) {
    println!("{:?}", self);
  }
}

impl<'r> Foo for &'r str {
  fn bar(&self) {
    println!("{:?}", self);
  }
}

fn main() {
  let first = 10;
  let second = "str";
  let vec : Vec<&Foo> = vec![&first, &second];
  for elem in vec {
    elem.bar();
  }
}
Это уже что-то, но выходит «bark the dog» — когда пишут dog.kick(human), хотя логичней human.kick(dog). Можно ли сделать что бы вызывать bar(elem)?
Тут проблема в том, что ни один способ вызова методов не универсален. Например с тем же bark(dog) было бы как-раз наоборот — именно «bark the dog» в прямом смысле, тогда как dog.bark() выглядет логично. Хотя Раст позволяет вызывать методы и так и этак, так что можете делать всегда так, как вам кажется правильнее. Хотя, как по мне, это лишает код единого стиля и несколько затрудняет его чтение и анализ в дальнейшем.
Dynamic dispatch на типажах реализуется через объекты типажей, подробнее можно посмотреть, например, здесь.

Перегрузки по аргументу нет.

То, что вы тут приводите, можно сделать так:

trait Base {
    fn print(&self);
}

struct Der1;

struct Der2;

impl Base for Der1 {
    fn print(&self) {
        println!("Der1")
    }
}

impl Base for Der2 {
    fn print(&self) {
        println!("Der2")
    }
}

fn main() {
    let d1 = Der1;
    let d2 = Der2;
    let v: Vec<&Base> = vec![&d1, &d2];
    for e in v {
        e.print()
    }
}


Соответстенно типаж лучше назвать не Base, а Printable, или как-то так, чтобы отражалась суть поддерживаемых операций.
Sign up to leave a comment.

Articles