Комментарии 119
Нет, конечно есть такие вещи которые лучше пересмотреть полностью и не повторять (например метапрограмминг на шаблонах в С++ — вместо него нужны нормальные синтаксические макросы), но самые простые основы, понятные всем и каждому, выкидывать все-же не стоит. Причем ломают-то именно «ради оригинальности» «чтобы было не как у всех», а не по какой-то осмысленной причине.
Многие ООП-решения подразумевают наличие нуллпоинтеров
Например? Опять же, разве в расте есть какие-то проблемы с Option<&T>
?
Ну с лайфтаймами — это общая "проблема" раста. (:
Новая "непонятная" сущность, но осваивать её, если хочется применять язык, всё равно придётся.
В остальном ладно, не буду спорить. Хотя всё равно не понял о каком именно "ООП коде" с нулевыми указателями речь.
Судя по тематическим вопросам на SO, народ на такое натыкается частенько. И ведь задает вопросы, и получает ответы, а не рыдает в днявки последними словами. Матчасть надо знать, вы правы 100%.
Бытует мнение, что агрегация лучше наследования. Хоть я и не очень люблю когда язык навязывает "единственный правильный способ", но плюсы у такого подхода имеются. Собственно, трейты раста могут сделать всё, что может "традиционное ООП", кроме наследования данных (не уверен, что это большая проблема). Наследование поведения есть.
Ну и, как по мне, нужен простой способ сказать, что мы хотим делегировать реализацию такого-то трейта такому-то полю структуры.
Касательно "почему нельзя добавить новое к существующему" — тогда рано или поздно мы получим не язык, а ужасного монстра. Сейчас и про С++ говорят, что знать его целиком невозможно. Насколько я понимаю, раст пытаются делать "простым", то есть не вводить 100500 вариантов синтаксического сахара для особых случаев и т.д. В принципе, такой подход мне по душе, хотя иногда грань между "простотой" и "примитивностью" довольно размыта.
Rust же задумывался как язык ограничений. При том что в нем сразу были продуманы и модульность, и синтаксические макросы, и функциональное программирование — тем ни менее было продумано и много ограничений. Не знаю, возможно кому-то это и нравится но мне не очень.
C++ получится «монстром» не потому что в нем 100500 вариантов синтаксического сахара, а потому что огромная его часть (метапрограмминг на шаблонах) была «случайно открыта», а не «спроектирована».
Более-менее согласен, но дело не в этом. Новые стандарты, даже в среде плюсовиков, периодически вызывают реакцию в духе "как теперь можно знать весь язык?". И я это мнение вполне понимаю, хоть и воспринимаю нововведения как "новые возможности", а не "новые сложности". И да, есть и упрощения и дополнительные удобства, но объём растёт, как растёт и сложность. Особенно, если учитывать легаси, благодаря которому забывать про устаревшие вещи так сразу нельзя.
Да, это путь любого живого языка, но я всё равно считаю, что подход "возьмём имеющийся язык и добавим туда чего-то" не идеален для создания новых. Выбрасывать тоже надо.
Касательно сахара: это я говорил именно о расте. С++, пожалуй, не самый подходящий (контр)пример, лучше будет сравнить, например, со Swift, где такого сахара довольно много. Вроде как удобно, но в то же время, такое разнообразие вызывает опасения. Впрочем, я на Swift не пишу и далеко идущие выводы делать не буду, тем более, что язык активно развивается.
Rust же задумывался как язык ограничений.
Пусть так, но я это воспринимаю как наличие предохранителя на оружии, а не как связанные за спиной руки. Есть unsafe, да и макросами много чего можно наворотить.
Более-менее согласен, но дело не в этом. Новые стандарты, даже в среде плюсовиков, периодически вызывают реакцию в духе «как теперь можно знать весь язык?».
По-моему, дело именно в этом. Сразу оговорюсь — С++ не мой основной язык, я его использую очень редко и знаю весьма посредственно. Но когда возникает необходимость разбирать чей-то код, вымораживает не обилие конструкций и способов что-то сделать, а именно китайский язык шаблонов, который будучи применен «нехорошо», напрочь убивает читаемость.
У меня С++ как раз основной язык. Не скажу, что с лёгкостью разбираюсь в потрохах буста, но "в среднем" шаблоны проблем не вызывают. Опять же, их недостатки — это продолжение достоинств. Скажем, концепты дадут более внятные сообщения об ошибках, но они не буду обязательными, да и в каких-то (пусть и примитивных) случаях кода наоборот станет больше.
В расте нет наследования типов и есть наследование типажей, потому что первое в C++ приводит к различным проблемам с памятью (каждый программист на C++ в этом месте может и захочет сказать: «да надо просто знать несколько простых правил, а если ты их не знаешь, то и нечего программировать, это азы языка», но это плохая контраргументация против «нужно знать несколько простых правил обращения с типами в раст»).
В расте это место намеренно упростили, в результате чего поверх структуры можно реализовывать любые интерфейсы, наследовать функциональность, но при этом: практически* любой объект можно удалить фактически простым free(), а скопировать — простым memcpy(). И не иметь при этом проблемы с памятью и головную боль с виртуальным наследованием, виртуальными деструкторами и вот этим вот всем.
* конечно, для объектов типа fd/socket придётся реализовывать типаж Drop — аналог dispose в C# и close() в Java.
Фактически это и есть си с классами, только лучше (вспомните, например, как си живёт со структурами типа sockaddr_in и sockaddr_in6 в стандартной библиотеке: они вообще никак не наследуются, но имеют обязательный список одинаковых первых полей, а в памяти кастуются к «базовому» классу).
но при этом: практически* любой объект можно удалить фактически простым free(), а скопировать — простым memcpy(). И не иметь при этом проблемы с памятью и головную боль с виртуальным наследованием, виртуальными деструкторами и вот этим вот всем.
А в с++ любой (правильно написанный) объект можно удалить простым delete, а скопировать — простым «a = b;». Проблемы с виртуальными функциями (да и всем остальным тоже) в с++ — от недопонимания. В другом яп будет другая реализация полиморфизма и точно так же найдутся люди которые не смогут её понять
Идея раста заключалась в том, чтобы не дать возможности случайно или по незнанию напороть ерунды в этом месте, неважно, понимаешь ты его полиморфизм или нет. Т.е. либо код не скомпилируется, либо какого-то проезда по памяти не будет.
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;
}
Это ни капельки не логично, если задуматься
Более чем, если задуматься еще раз.
Более того, не знаю, что вы имеете в виду под «особой» инициализацией, но нам достаточно иметь в классе любое динамическое выделение памяти, чтобы получить отличный проезд:
Его и имею в виду. Делаешь инициализацию чего-то динамического — буфер, объект, что-то еще — имей деструктор.
Мы знаем, что есть класс, у него есть поля и методы. Мы можем создавать новые объекты оператором new и удалять — оператором delete. Можно создать поле-объект в конструкторе и удалить в деструкторе.
Ещё мы знаем, что классы можно наследовать. При этом если мы конструируем класс-наследник, у него вызовется и конструктор базового класса, и конструктор наследника. Аналогично с деструктором, мы просто унаследовались, и вот уже разрушается и наследник, и базовый класс.
Конструкторы и деструкторы для нас при этом отделены от обычных функций: мы знаем, что они вызываются каскадом, а обычная унаследованная функция не будет звать функцию базового класса, если только в ней явно этот вызов не написать. И вот в этом месте таится наш подвох: мы знаем, что есть виртуальные функции, которые нужны, чтобы из базового класса звать реализацию наследника, но при этом и в голову не придёт, что то же самое может быть нужно для деструктора (тем более, что виртуальных конструкторов вообще не бывает), потому что он вроде как и так зовёт обе реализации.
Более того, конкретно в этом месте можно напороться ещё и потому, что общепринятая инструкция про виртуальные деструкторы ничего не говорит про динамическое выделение памяти, а учит, что виртуальный деструктор вам нужен, если вы пишете хотя бы одну чисто виртуальную функцию. А у нас таких и нет.
Вас бесит, что поведение обычных и виртуальных деструкторов отличается? Нас ещё на втором занятии в универе научили: «Если наследуешься от класса, у которого есть данные, делай его деструктор виртуальным. Короче, делай его виртуальным всегда.»
В Rust тоже есть бесящие концепции. Например, отсутствие переиспользования кода. Ага, та самая претензая про treats, ни тебе реализаций по умолчанию, ни полей. Например, сложности разрешения согласованности типажей, типов и реализаций. Ага, та самая, из-за которой добавили #[fundamental].
Давайте поговорим про них, так как виртуальные деструкторы уже изжили себя как проблема.
Просто переопределите реализацию по умолчанию. Вы не сможете. У вас здесь даже нет «реализации по умолчанию», вам нужно явно указать «вот эту реализацию, пожалуйста». Каждый раз. Для каждого реализуемого класса. А если реализация разбита по частям…
Это фича языка, он так задуман, но вот эта вот проблема ни капли не сравнится с неудобством объявления одного единственного деструктора виртуальным.
«И вот эти люди запрещают нам ковыряться в носу» ©
Опять же, всё написанное выше справедливо для сравнения с плюсами, у чистых Сей нет даже такого инструментария, а любые поделки на тему ООП получаются монструозными вурдалаками с кастами указателей на указатели на функции между собой. Но ведь не я это затеял.
Я даже не говорю о том, что вы выворачиваете внутреннюю структуру классов наружу, портя красивый пользовательский интерфейс.
Если библиотека на С++ разрешает наследование от класса, то это тоже выворачивает внутреннюю структуру класса наружу. Не вижу особой разницы. Доступ ко внутренностям реализации в Rust можно ограничить на уровне модуля.
Просто переопределите реализацию по умолчанию. Вы не сможете.
В смысле? Переопределенная реализация трейта, имеющего реализацию по умолчанию
вам нужно явно указать «вот эту реализацию, пожалуйста».
И? Несколько строк для указания какие поля нужно использовать и impl ThisTrait for MyShinyStruct {}
вместо class MyShinyClass: public ThisBaseClass
и кучи проблем с множественным наследованием.
Для каждого реализуемого класса. А если реализация разбита по частям…
По-моему, глубокие иерархии классов довольно давно считаются плохой идеей. Так что не вижу ничего страшного в том, чтобы для реализации плохих идей требовалось больше работать руками.
Насчет реализации разбитой по частям не понял.
но вот эта вот проблема ни капли не сравнится с неудобством объявления одного единственного деструктора виртуальным.
Эта "проблема" ни в коем случае не вызовет некорректного поведения программы, в отличии от. Не забывайте, что использовать С++ без статического анализатора опасно для ног.
Эта «проблема» ни в коем случае не вызовет некорректного поведения программы, в отличии от.Не забывайте, что мы говорим о разноуровневых проблемах. Сопоставимой проблемой будет, например, подключить не ту реализацию или потерять владение объектом. Ведь именно об этом весь сыр-бор — подключение не той стандартной реализации деструктора, разве нет?
По-моему, глубокие иерархии классов довольно давно считаются плохой идеей.Глубокие — это сколько? Я знаю, что на эту тему высказывают некоторые эксперты и «эксперты». Главное, что я слышал — «следуйте здравму смыслу».
Несколько строк для указания какие поля нужно использовать и <...> вместо <...> кучи проблем с множественным наследованием.
У меня есть класс Сообщение, есть его потомки Входящее и Исходящее. У них есть базовые реализации Чтения и Записи. И есть под пару тысяч потомков-сообщений, каждое из которых использует и родительские поля, и родительские методы, в том числе, перегруженные. Да, их все можно переписать без ООП, но ИМХО
куда менее изящно. Структур будет больше, кода будет больше, а производительность — та же самая, потому что компилятор всё преобразует в код без ООП, а -O3 всё отлично упаковывает. Здравый смысл подсказывает, что оно мне не надо длинно и муторно при том же выхлопе.
В смысле? Переопределенная реализация трейта, имеющего реализацию по умолчаниюВ смысле вы обязаны написать impl UsesFields for MyStruct {} В данном случае, это явный выбор определённой реализации, построенной только на интерфейсе, а не получение реализации по умолчанию, разве не так? К тому же, у вас нет доступа к базовой реализации после её переопределения. Вы просто получаете другую функцию на этапе компиляции и баста.
В Rust просто всё сделано иначе. И плюсовые подходы не работают. Я не понимаю, почему вы пытаетесь доказать обратное.
impl<T> UsesFields for T {}
без дальнейших действий с точки зрения MyStruct. Частные специализации её просто перекроют, как шаблоны в 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 — классическая задача, которую принято решать с помощью ООП, но это не значит, что не стоит пытаться решить её другим способом.
Вы путаете интерфейсы и структуры. Вы можете наследовать все интерфейсы и, соответственно, иметь все сетеры-гетеры, но вы не наследуете структуры. Вы можете иметь гарантии, что объект имеет указанное поведение, но не можете иметь гарантии, что что-то внутри устроено именно так, как описано, особенно, если это чёрный ящик. Между этими двумя понятиями огромная пропасть. У вас есть гарантия, что что-то, что кладут в Vec<Box> ведёт себя как Says, но нет никаких гарантий, что он вообще будет иметь непустую структуру. С одной стороны, это не такая уж большая проблема. С другой стороны, все эти селекционные чудеса, приправленные шаблонной магией противопоставляются одному единственному виртуальному деструктору…
Или вот так: https://play.rust-lang.org/?gist=db178ae263364d0839e6aded3de8f2b3&version=stable&backtrace=0
Кстати, как это будет выглядеть в C++?
Кстати, как это будет выглядеть в 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();
}
};
И теперь Says не имеет доступа к данным из Base. С чего собственно всё и начиналось. iCpu хотел иметь доступ из Rust'ового трейта к данным.
Мешает то, что такой возможности (пока?) нет https://github.com/rust-lang/rfcs/pull/52
Ещё очень не помешала бы возможность делегировать реализацию трейта элементу структуры. https://github.com/rust-lang/rfcs/pull/1406
Но с этим придётся подождать.
Вся моя аргументация относится к исходному поинту дискуссии: в C++ есть потенциальные грабли с памятью при таком наследовании, неважно, неопытный/неумелый ты программист, или баг не отследили в процессе рефакторинг, важно, что этот проезд достаточно легко стриггерить. И система наследования в расте сделана так, чтобы этого проезда избежать, а не чтобы «не как в C++». Вот и всё.
Что до исходной аргументации, она так же не к месту. В Rust нет полноценного наследования. Есть реализация интерфейсов, то есть АОП, но не ООП. Сравнение плюсов с растам вообще не имеет смысла в этом контексте, ведь если наложить на плюсы те ограничения, которые действуют в расте, виртуальные деструкторы плюсам просто не потребуются. А если не накладывать, то расширение функциональности влечёт за собой и размножение грабель. Как и увеличение числа моделей работы с памятью в расте влечёт за собой проблемы новичков с потерей прав владения или неконтролируемое дублирование памяти по делу и без. Вот и всё.
Мы не можем сказать, что было мотивом именно такой реализации системы наследования в расте
А как же 100500 RFC и открытых обсуждений на эту тему?
Не, ну в голове-то он может думать что угодно, но процесс обсуждения и разработки в целом весь на гитхабе и на internals.rust-lang.org есть :) Оригинальное обсуждение дизайна я не нашёл, но вот, например, очень интересная старенькая дискуссия про добавление наследования структур. Оттуда ещё и можно ещё глубже по ссылкам сходить.
> Что до исходной аргументации, она так же не к месту. В Rust нет полноценного наследования. Есть реализация интерфейсов, то есть АОП, но не ООП. Сравнение плюсов с растам вообще не имеет смысла в этом контексте
Это, имхо, уже демагогия. Классическое наследование в ООП подразумевает наследование данных и поведения, в расте есть второе, но нет первого. Я слабо знаком с АОП и мне кажется, что оно вообще не про то, но готов поверить на слово. В любом случае, мы вынуждены сравнивать, просто потому что используем язык для решения задачи, а не потому что «там ООП». Даже исходный тред NeoCode начинается именно с жалобы, что ООП не такой.
Наложенные ограничения — это причина разницы, мы же работаем с последствиями.
АОП действительно немного не про то, в нём объект строится из кусков, обладающих и поведением, и данными, а доступ к ним производится по запросу «обладает ли таким куском». В Rust же, по большому счёту, обыкновенный ООП, просто пропущенный через MVC-мясорубку, которая раскидала отдельно методы, отдельно данные и отдельно итерфейс. Я бы не сказал, что результат получился прям уж хорошим.
Что до "мы вынуждены сравнивать", давайте сравнивать всё по объективным параметрам. И в их числе должны быть не только «простота допущения ошибки», но и «простота её поиска\исправления», и «накладные расходы по её автоматическому выявлению».
Это не ООП, пропущенный через мясорубку. Мир на ООП не кончается. Если упрощенно, то это реализация typeclass'ов из Haskell'я для императивного языка.
Это уже больше переход в обсуждение «чья реализация оказалась лучше», а не «почему так», я, пожалуй, в ней участвовать не буду :) Хорошо, что мы под конец друг друга поняли.
Попробуйте представить, что вы зелёный новичок, а я попробую рассказать, как он может воспринимать это место в 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: хабр сожрал шаблоны. Ненасытный.
первое в 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 уже
Ну так там еще только ограниченый вариант.
Вариант 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
рустовики
\ˈrəst\ :)
не любят D
Мотив "всем привет, а зачем вы написали Rust, раз уже давно есть D? — Он завязан на GC. — GC отключаемый. — Без GC D теряет большую часть достоинств" часто повторяется.
Наверное, с тех пор, как Александреску сказал, что он не нужон :)
Его критика и правда была очень спорной.
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
}
}
}
Разве в том же С++ базовый класс может дотянуться до полей в потомках? Или в каком-то другом популярном языке так можно?
Разработчики же языка говорят, что это было сделано специально — требование типажа по наличию поля в реализующем классе может ограничить его применимость.
Ну и трейты там же имеют право обращаться к любым свойствам и методам подмешивающего их класса-или-объекта. Если же свойства или метода вдруг нет — fatal error. Разумеется, тоже рантайм.
Впрочем вы, очевидно, ждали ответ про compile-time. Такого, насколько я знаю, нет нигде. Буду рад, если кто-то укажет на пробел в моих знаниях.
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 создающие "объекты", ну и функции по работе с ними. Если язык позволяет, то гораздо удобнее оформить это в виде класса с деструкторами, чтобы исключить возможность забыть об освобождении ресурса.
Ну и в третьих, если писать обёртки к С++ либам, то их всё равно нужно собирать под каждую платформу. Обычно эти проблемы перекладываются на пользователей наших обёрток.
И на D, вроде как, список и кончается.
Кстати, недавно выкатили вот такое расширение: https://github.com/google/rustcxx. Интересно, выдет ли из него чего-то толковое в итоге?
Их давно уже можно подключать, только не все С++ идиомы поддерживаются. А вот C можно подключать вполне прозрачно. Собственно стандартная библиотека C доступна через модуль core.stdc
.
Если речь о Calypso, то это не столько фича D сколько LLVM, ну и работает (естественно) не с любым компилятором.
Зачем просто портировать библиотеку с С++ на раст?
а как иначе вы переиспользуете API на c++-ных шаблонах?
Почему я отказался от Rust