Pull to refresh

Comments 119

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

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

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

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


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

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

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


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


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

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

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


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


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


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

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

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

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

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

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

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

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

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

А в с++ любой (правильно написанный) объект можно удалить простым delete, а скопировать — простым «a = b;». Проблемы с виртуальными функциями (да и всем остальным тоже) в с++ — от недопонимания. В другом яп будет другая реализация полиморфизма и точно так же найдутся люди которые не смогут её понять
Вот в этом уточнении: «правильно написанный» — вся соль :)
Идея раста заключалась в том, чтобы не дать возможности случайно или по незнанию напороть ерунды в этом месте, неважно, понимаешь ты его полиморфизм или нет. Т.е. либо код не скомпилируется, либо какого-то проезда по памяти не будет.
Не любой объект можно удалить delete, а только такой, класс которого в месте удаления точно известен компилятору или у него есть виртуальный деструктор.
Логично, что виртуальный деструктор должен быть, если мы предполагаем передачу ссылок типа родительского объекта. Как иначе? Если у нас есть особая инициализация, то и особый деструктор потребуется. Если нет, то и стандартный справится.
Это ни капельки не логично, если задуматься. Более того, не знаю, что вы имеете в виду под «особой» инициализацией, но нам достаточно иметь в классе любое динамическое выделение памяти, чтобы получить отличный проезд:
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;
}
Это ни капельки не логично, если задуматься

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

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

Его и имею в виду. Делаешь инициализацию чего-то динамического — буфер, объект, что-то еще — имей деструктор.
Деструктор есть. Но он внезапно должен быть виртуальным, чтобы такого не происходило.
Почему внезапно? Это — логично. Делаешь динамическую инициализацию — позаботься о динамической деинициализации. Что здесь «такого»?
Давайте попробуем сначала :) Попробуйте представить, что вы зелёный новичок, а я попробую рассказать, как он может воспринимать это место в C++.
Мы знаем, что есть класс, у него есть поля и методы. Мы можем создавать новые объекты оператором new и удалять — оператором delete. Можно создать поле-объект в конструкторе и удалить в деструкторе.
Ещё мы знаем, что классы можно наследовать. При этом если мы конструируем класс-наследник, у него вызовется и конструктор базового класса, и конструктор наследника. Аналогично с деструктором, мы просто унаследовались, и вот уже разрушается и наследник, и базовый класс.
Конструкторы и деструкторы для нас при этом отделены от обычных функций: мы знаем, что они вызываются каскадом, а обычная унаследованная функция не будет звать функцию базового класса, если только в ней явно этот вызов не написать. И вот в этом месте таится наш подвох: мы знаем, что есть виртуальные функции, которые нужны, чтобы из базового класса звать реализацию наследника, но при этом и в голову не придёт, что то же самое может быть нужно для деструктора (тем более, что виртуальных конструкторов вообще не бывает), потому что он вроде как и так зовёт обе реализации.
Более того, конкретно в этом месте можно напороться ещё и потому, что общепринятая инструкция про виртуальные деструкторы ничего не говорит про динамическое выделение памяти, а учит, что виртуальный деструктор вам нужен, если вы пишете хотя бы одну чисто виртуальную функцию. А у нас таких и нет.
У всех языков есть свои недостатки и есть «не баги, а фичи». Виртуальные деструкторы из этой серии.
Вас бесит, что поведение обычных и виртуальных деструкторов отличается? Нас ещё на втором занятии в универе научили: «Если наследуешься от класса, у которого есть данные, делай его деструктор виртуальным. Короче, делай его виртуальным всегда.»
В Rust тоже есть бесящие концепции. Например, отсутствие переиспользования кода. Ага, та самая претензая про treats, ни тебе реализаций по умолчанию, ни полей. Например, сложности разрешения согласованности типажей, типов и реализаций. Ага, та самая, из-за которой добавили #[fundamental].
Давайте поговорим про них, так как виртуальные деструкторы уже изжили себя как проблема.

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

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

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

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

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


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

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


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

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


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

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


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


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

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

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

В Rust просто всё сделано иначе. И плюсовые подходы не работают. Я не понимаю, почему вы пытаетесь доказать обратное.
Я тут мимо проходил, просто замечу, что реализация по умолчанию в расте делается один раз автором UsesFields как
impl<T> UsesFields for T {}

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

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

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

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


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

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


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

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

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


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

Самое противное в вашем коде — он делает вид, что реализует то, что вы хотите мне показать. Но не реализует.
Вы забыли одну тонкую вещь. Содержит != является. Вы не можете сделать
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 при каждом вызове этого метода. Это очень сложно проконтролировать, если вообще возможно.

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

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


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

Очень даже могу.
Почему там 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 — классическая задача, которую принято решать с помощью ООП, но это не значит, что не стоит пытаться решить её другим способом.

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

Вы путаете интерфейсы и структуры. Вы можете наследовать все интерфейсы и, соответственно, иметь все сетеры-гетеры, но вы не наследуете структуры. Вы можете иметь гарантии, что объект имеет указанное поведение, но не можете иметь гарантии, что что-то внутри устроено именно так, как описано, особенно, если это чёрный ящик. Между этими двумя понятиями огромная пропасть. У вас есть гарантия, что что-то, что кладут в Vec<Box> ведёт себя как Says, но нет никаких гарантий, что он вообще будет иметь непустую структуру. С одной стороны, это не такая уж большая проблема. С другой стороны, все эти селекционные чудеса, приправленные шаблонной магией противопоставляются одному единственному виртуальному деструктору…
Кстати, как это будет выглядеть в C++?

struct Base {
    virtual void foo() {
        cout << "Base::foo()" << endl;
    }

    virtual void bar() {
        cout << "Base::bar()" << endl;
    }
};

struct Derived : Base {
    void bar() override {
        cout << "Derived::bar()" << endl;
        Base::bar();
    }
};

http://ideone.com/Zc4TRj

UFO just landed and posted this here

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

UFO just landed and posted this here
UFO just landed and posted this here
а если в классе нет данных (чисто интерфейсный) то виртуальный деструктор тем более нужен
Кроме тех случаев, когда класс предоставляет обёртку функций (тогда нужно посвятить автора в концепцию пространства имён).
Или когда он не имеет потомков (привет от шаблонов с их operator() и typedef using).
Или когда он является промежуточным классом в иерархии наследования.
Страуструп с вами, я не говорю, что виртуальный деструктор — это какая-то бага, что она меня бесит или что-то ещё в этом же духе.
Вся моя аргументация относится к исходному поинту дискуссии: в C++ есть потенциальные грабли с памятью при таком наследовании, неважно, неопытный/неумелый ты программист, или баг не отследили в процессе рефакторинг, важно, что этот проезд достаточно легко стриггерить. И система наследования в расте сделана так, чтобы этого проезда избежать, а не чтобы «не как в C++». Вот и всё.
Мы не можем сказать, что было мотивом именно такой реализации системы наследования в расте, и не думаю, что, к примеру, Хор через пару дюжин дней или лет признается: «да, мне тупо не нравилось наследование в плюсах, потому я и навалял что угодно, лишь бы не плюсовое [РосКомНадзор]», даже если изначально оно так и было.

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

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

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

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

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

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

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

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

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

struct -> Model -> Модель данных
trait -> View -> Пользовательский интерфейс
impl -> Controller -> Внутренняя логика
> И в их числе должны быть не только «простота допущения ошибки», но и «простота её поиска\исправления», и «накладные расходы по её автоматическому выявлению»…

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

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


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

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

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

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

> Это, конечно, может быть проблемой
Главная на самом деле проблема — это то, что такое знание энфорсится в программиста. Т.е. это правило написано в каких-то книжках, на любом форуме вам авторитетно объяснят, что вы идиот и должны сначала были читать учебник, но в лучших традициях 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). Но это уже совсем другая история.
Ну, не знаю, мне кажется, что новичок вполне может сразу гуй писать. Это студенту такое не дадут, а заставят до посинения писать алгоритмы сортировки, но студент != новичок.

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

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


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

Я сказал, что ваш аргумент фактически сводится к этому.
И вот оно, непонимание! Поприветствуйте, дамы и господа!
У вас в 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: хабр сожрал шаблоны. Ненасытный.
Да, но ваш код отработает некорректно: B::x не будет освобождена: код. Единственный вариант получить то, что нужно, без виртуального деструктора, будет выглядеть так: код

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

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

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

Есть две основные проблемы — копирование объектов при передаче их в качестве параметра и безконтрольное копирование указателей. Так же при наследовании очень часто забывают сделать деструктор базового класса виртуальным, из-за чего он не вызывается из кода потомков.
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) Практически все компиляторы сообщают, если базовый класс содержит поля и не содержит виртуальный деструктор.
Почему нельзя просто добавить новое к существующему.

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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


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

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


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

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

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

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

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

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

UFO just landed and posted this here
рустовики

\ˈrəst\ :)


не любят D

Мотив "всем привет, а зачем вы написали Rust, раз уже давно есть D? — Он завязан на GC. — GC отключаемый. — Без GC D теряет большую часть достоинств" часто повторяется.


Наверное, с тех пор, как Александреску сказал, что он не нужон :)

Его критика и правда была очень спорной.

Он теряет достоинства связанные с GC, то есть только те, которых у Rust нет в принципе :-) В D реализованы чудесные шаблоны и исполнение кода времени компиляции, а в Rust — сопоставление с образом и контроль владения. Их бы объединить — была бы бомба :-)

Насколько я помню там терялась и значительная часть стандартной библиотеки. Или уже переписали?

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

UFO just landed and posted this here
У меня тупой вопрос по вот этому куску кода:
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
        }
    }
}


Разве в том же С++ базовый класс может дотянуться до полей в потомках? Или в каком-то другом популярном языке так можно?
UFO just landed and posted this here
Но поля должны быть в том же базовом классе, а не в потомке. А в этом трейте ничего нет. Поэтому мне кажется странным, что автор так бурно удивляется ошибке компиляции.
UFO just landed and posted this here
Как я понимаю, он больше удивляется как раз тому, что нет возможности как-то задать поля для типажей. Как следствие — сильно ограничена возможность писать дефолтные функции для самого типажа и приходится прописывать ее отдельно для каждого реализующего типаж класса.
Разработчики же языка говорят, что это было сделано специально — требование типажа по наличию поля в реализующем классе может ограничить его применимость.
Если речь о полях класса (статические свойства), то да — в PHP такое сплошь и рядом, называется «позднее статическое связывание». Разумеется, это исключительно рантайм-фича (а как иначе?)

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

Впрочем вы, очевидно, ждали ответ про compile-time. Такого, насколько я знаю, нет нигде. Буду рад, если кто-то укажет на пробел в моих знаниях.
Пожалуй, единственное, что мне все же приходит в голову — это 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;
}

В 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!" );
    }
}
Просто нет простого способа указать компилятору не удалять переменную автоматически когда она выходит из области видимости.

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

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


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

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

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


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


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

UFO just landed and posted this here

И на D, вроде как, список и кончается.


Кстати, недавно выкатили вот такое расширение: https://github.com/google/rustcxx. Интересно, выдет ли из него чего-то толковое в итоге?

UFO just landed and posted this here

Больше, ей 5 лет минимум :-)

Если речь о Calypso, то это не столько фича D сколько LLVM, ну и работает (естественно) не с любым компилятором.

Зачем просто портировать библиотеку с С++ на раст?

а как иначе вы переиспользуете API на c++-ных шаблонах?
Хотел посмотреть на это nangui-rust. К сожалению, не билдится ни сама либа ни примеры.
Sign up to leave a comment.

Articles

Change theme settings