Как же предлагают решить эту проблему разработчики языка?
На сайте CppCoreGuidelines есть несколько любопытных документов (здесь и здесь).
Основной посыл таков: мне не можем реализовать безопасные невладеющие указатели, не нарушая zero overhead principle. Поэтому мы не будем реализовывать такие средства в runtime, а постараемся решить проблему статическим анализом кода.
Я не согласен с такой позицией и вот мои доводы:
- Статический анализ не сможет отловить все проблемы. Сами авторы статей говорят о том, что иногда прийдется помечать некоторые конструкции как безопасные, оставляя вопрос корректности кода на совести разработчика.
- Для того, что бы статический анализ заработал, нужна поддержка со стороны компиляторов. Когда она появится — неизвестно.
- Статический анализ не позволяет иметь слабые невладеющие указатели (аналог weak_ptr, который зануляется при удалении объекта).
- Реализация таких указателей нарушает zero overhead principle — это правда. Но, на мой взгляд, это принцип также нарушают shared_ptr, т.к. имеет дополнительный ref_count объект или, например string с его small string optimization. В любом случае, стандартная библиотека предоставляет классы с четко описанными характеристиками, а программист решает подходят эти классы для него или нет.
По моему мнению, стандартная библиотека должна предоставлять классы для всех основных сценариев программирования. Невладеющие указатели и доступ по висячим ссылкам большая и до сих пор незакрытая тема в С++. Думаю стандартная библиотека должна предоставлять программисту опциональную возможность иметь такие указатели.
В своем проекте RSL я попробовал реализовать такой указатель. Основная идея не нова: необходим объект, который при разрушении будет нотифицировать указатели о факте удаления.
Таким образом мы имеем два класса:
- rsl::track::trackable — класс, который оповещает указатели при удалении.
- rsl::track::pointer — собственно невладеющий указатель.
Когда rsl::track::pointer'ы указывают на один и тот же rsl::track::trackable объект, они выстраиваются в двухсвязный список. Указатель на голову списка содержится в rsl::track::trackable. Таким образом, создание указателей занимает константное время. Размер rsl::track::trackable составляет один указатель, а rsl::track::pointer — 4 указателя (указатель на объект, два указателя для организации списка и еще один для реализации полиморфного поведения). Возможно более оптимальная организация указателей, если кто знает, прошу рассказать.
Так же данная реализация не потокобезопасна, для обеспечения работы в разных потоках прийдется добавлять std::atomic_flag и замедлять модификацию указателей.
Кроме того, с появлением allocator aware containers, появилась возможность реализовать аллокатор, который позволяет использовать rsl::track::pointer со стандартными контейнерами. Основная идея в том, что теперь все аллокации в контейнерах делаются экземпляром аллокатора, хранящегося в контейнере, или его шаблонной копии, и мы можем хранить rsl::track::trackable в аллокаторе и передавать его в копии.
В тестах приведены примеры работы с основными стандартными контейнерами, включая std::array, а также unique_ptr.
В заключение хочу привести еще один сценарий, в котором rsl::track::pointer будут полезны. Это ситуации, аналогичные delete this. Обычно такое происходит косвенно при вызове внешнего по отношению к объекту кода, функтора или сигнала. Такого рода ошибки редки, но трудно уловимы.
Для таких случаев (да и любых других проблем с доступом по висячей ссылке) используют такие средства как google sanitizers, которые позволяют отлавливать подобные проблемы.
Но эти средства имеют свои недостатки:
- Работают не везде (не на всех платформах).
- Необходима инструментализация кода — код отличается от продакшена.
- Нет аналога weak_ptr, указателя котроый зануляется при удалении объекта.
- Детектирует время и место доступа по невалидному указателю. Хотя, как правило, более интересен момент, когда происходит удаление объекта, на который есть указатели. По этой же причине инструментация детектирует не все случаи неправильного доступа, а лишь те, которые реально отработали при тестировании.
Надеюсь библиотека окажется полезной С++ разработчикам. Возможно есть другие способы решения подобных проблем, с удовольствием выслушаю в комментариях.
P.S. Еще раз, для удобства, привожу ссылку на код библиотеки RSL.
Комментарии (127)
Satus
03.04.2017 15:01Прочитал статью два раза, но так и не понял, в чём суть проблемы. Можете прояснить?
lexxmark
03.04.2017 15:18+1Если вы программируете на С++, то понятие dangling pointer вам должно быть знакомо. Коротко — в С++ нет средств для отлова ситуаций, когда у нас есть указатель на удаленный объект.
LooMaclin
03.04.2017 15:28+1Попробуйте Rust.
lexxmark
03.04.2017 21:13+1Да это понятно: о) Но куда уж мы от С++?: о)
TargetSan
03.04.2017 22:15-1Golang, Swift своим вполне себе успехом показывают, что много куда — было бы желание. И если Swift насаждается Apple как монополистом платформы, то Гофер хорошо выстрелил без такого "допинга". Любой успех начинается с малого. А если думать в терминах "куда же мы от Х", то так и будем ещё лет 10-20 с перераздутым стандартом, убогими iostreams и зета-функцией Римана.
P.S: Эх, где был нынешний bindgen и tokio два года назад...
lexxmark
04.04.2017 08:41+1Эволюция наше всё!
Посоветуйте Линусу слезть с С и переползать на Golang или Rust. Думаю узнаете много идиоматических финских слов: о) (шутка).
В общем-то, поддерживать старые проекты и плавно переводить их на новые плюшки языка — вполне себе выход во многих случаях.
P.S: Эх, где был нынешний bindgen и tokio два года назад...
Вот здесь не уловил.TargetSan
04.04.2017 08:48+1Что я точно не предлагаю — переписывать объёмные проекты. Куча муторной и часто ненужной работы. Вот писать новые модули ядра на Rust вроде как пытаются.
bindgen — пакет Rust для генерации биндингов к С библиотекам по заголовочным файлам.
tokio — асинхронный стек на основе фьючерсов для того же Rust.
Жалуюсь потому, что 2 года назад первый был в зачаточном состоянии, даже константы из макро-дефайнов транслировать не умел, а второго не было в принципе. В противном случае на один новый проект, написанный на Rust вместо С++, было бы больше.kmu1990
04.04.2017 09:24-1Кто-то пытается для развлечения, но в само ядро эти модули не войдут (по крайней мере не в ближайшем будущем), да и пользоваться Rust-ом просто не удобно в этом случае. Модуль ядра не работает в воздухе, ему нужно окружение — интерфейсы ядра, оборачивать все эти интферфейсы в Rust значит делать двойную работу (с учетом того, что внутри ядра не поддерживается стабильный API, я уже молчу про ABI).
В дополнение к этому идеология Rust («безопасность» через кучу ограничений), просто не согласуется с идеологией ядра Linux (не знаешь что делаешь — не суй свой нос). Да и касательно отлова проблем с памятью, в ядре Linux есть довольно хорошие средства нахождения таких проблем, Rust мало что к этому может добавить. Да и проблемы неправильного использования памяти, вероятно, самый простой класс проблем, с которыми приходится сталкиваться в ядре, но Rust не предлагает никакого решения для большинства из других проблем.
Satus
03.04.2017 15:41+1Ну я знаю, что такое dangling pointer. Но разве shared_ptr и weak_ptr — не средства как раз для того, чтобы у вас не было таких указателей?
TargetSan
03.04.2017 15:52Нет. Они предназначены для общего владения одним объектом из нескольких мест. Но проблему "утёкшего" временного указателя не решают.
Satus
03.04.2017 15:54+2Почему нет? Если все shared_ptr были удалены, вы не сможете залочить weak_ptr.
TargetSan
03.04.2017 16:03Сходу могу вспомнить 2 фактора
- Оверхед на подсчёт ссылок
- Не работает с объектами на стеке
Satus
03.04.2017 16:29+21. Так ведь и предложенное решение не compile-time. Я бы даже готов поспорить, что оно медленнее.
2. Зачем брать указатели на объекты на стеке? Чтобы обратиться по указателю к объекту на стеке, который уже удалён, надо ещё постараться.TargetSan
03.04.2017 16:49- Я кстати не говорил, что предложенное решение хорошее. И таки оверхед у него будет поболе чем у shared_ptr.
- А как вы предлагаете передавать объекты не "с потрохами"? Ссылки, из-за своей недвижимой природы, тоже не всегда подходят.
На самом деле, проблема более фундаментальна, чем просто подсчёт ссылок на обекты в куче. Умные указатели в реальном коде решат часть ваших проблем — но далеко не все, и далеко не все решённые проблемы будут из числа самых коварных.
Satus
03.04.2017 17:01А как вы предлагаете передавать объекты не «с потрохами»? Ссылки, из-за своей недвижимой природы, тоже не всегда подходят.
Не совсем понимаю. Допустим, у вас есть какая-то функция function(), которая у себя внутри вызывает функцию do_something( MyStruct* params ), где MyStruct — какие-то опциональные параметры в виде структуры. Что мешает мне сделать
В чём тут может быть проблема? Или я что-то не понимаю?void function() { MyStruct s; // заполняем s do_something( &s ); // или do_something( nullptr ), если не передаём ничего }
TargetSan
03.04.2017 17:14Проблема в том, что в общем случае вы не знаете, куда потом денется
&s
.
Естественно, что на простых примерах всё очевидно. Менее простой пример (тоже искусственный, конечно) — инвалидация итераторов:
std::vector<Foo> foos; std::vector<Bar> bars; auto const& first = foos.first(); for (auto const& bar: bars) { if(asFoo(bar) != first) foos.push_back(asFoo(bar)); }
Ещё один пример (опять же псевдокод):
for(auto& item: range(returnVector()) | filter(userPredicate)) { // Here, item's would point to nowhere, since initial container of returnVector is already dead }
В общем, проблема "висящих ссылок" кажется мелкой, но вездесущей. Когда идёшь по набитому тараканами подвалу, сложно не наступить ни на одного.
Satus
03.04.2017 17:55В первом случае правила инвалидации итераторов детально описаны для каждой структуры данных. Тут проблема скорее не с висящими ссылками, а с неверным использованием структуры данных. Хотя я сам попадался на эту ошибку пару раз, но решать эту проблему добавлением runtime-оверхеда как-то сомнительно, когда её можно вполне решить compile-time проверками.
Второй случай интереснее. Потому что помимо классической reference to temporary встаёт вопрос, как работает filter и range. Если они делают копии, то проблемы не будет. Если нет, то опять таки, это можно проверить на этапе компиляции.TargetSan
03.04.2017 18:28+1А вот сейчас вы спорите не с тем человеком :) Ваш изначальный посыл был, что смарт-указатели решают эту проблему. Я вам отвечал, что есть целый пласт ситуаций, где не могут — поскольку отношение заимствования не может быть выражено в системе типов С++, но само заимствование присутствует повсеместно.
По моему опыту, данная проблема не может быть решена в компайл-тайме в рамках правил С++. Из-за того, что копирование, а не перемещение, первично, такой "трекинг" нельзя добавить в компилятор, не ломая всё.
Satus
03.04.2017 21:20Да я не то чтобы спорю. Я просто пытаюсь понять, что и зачем сделал автор этой статьи ;)
lexxmark
03.04.2017 21:33Вот пример из GUI:
void Button::DoClick() { // do some stuff ... // emit signal click.invoke(); // do some other stuff // здесь this может быть удален каким-то обработчиком click // и мы упадем ... }
Что бы отловить проблему и понять кто удаляет нас можно написать:
void Button::DoClick() { // мы должны жить до конца функции rsl::track::pointer<const Button, assert_on_dangle> guard(this); // do some stuff ... // emit signal click.invoke(); // do some other stuff // здесь this может быть удален каким-то обработчиком click // и мы упадем ... }
Вы какими средствами пользуетсь для решения подобных проблем?
Как уточнял коллега выше — shared_ptr не панацея.Satus
03.04.2017 21:51Удалять this изнутри себя, а потом обращаться по нему — баг в логике программы. Такое обычно решается тем, что удаление объекта откладывается до конца обработки события или до следующего фрейма. Все, кто писал на Qt, знают, что делает функция deleteLater() у QObject'a.
lexxmark
03.04.2017 23:18Именно баг! Вопрос, как его ловить?
deleteLater вещь хорошая, но она решает симптомы, а не причину. По сути это garbage collection — мы не знаем когда можем удалять объект, так давай те удалим его как-нибудь потом. Хотя признаюсь — сам иногда пользуюсь им.Satus
03.04.2017 23:47Ловить можно с помощью таких утилит, как MemorySanitizer, valgrind, Dr.Memory и прочих. И просто взять за правило, что нельзя удалять объект, а потом к нему обращаться.
deleteLater — не GC. Вы точно знаете, когда объект будет удалён — при следующем цикле обработчика событий.lexxmark
04.04.2017 09:05Ловить можно с помощью таких утилит, как MemorySanitizer, valgrind, Dr.Memory и прочих.
Про них я упомянул в статье со списком недостатков, по моему мнению.
И просто взять за правило, что нельзя удалять объект, а потом к нему обращаться.
Это хорошое правило и самоочевидное правило. Так же можно посоветовать соблюдать ПДД чтобы никогда не попадать в аварии. Но мы то знаем, что жизнь сложнее и соблюдение правил не залог безопасности. Снизить вероятность можно, полностью исключить проблем — нельзя.
deleteLater — не GC.
Ну почему же, можно на него посмотреть с этой точки зрения. Вместо удаления объектов, которые нам не нужны, мы помечаем их как неиспользуемые и оставляем в памяти. Ни о каком RAII при этом речь уже идти не может (что также верно и для Java/C#).
Вы точно знаете, когда объект будет удалён — при следующем цикле обработчика событий.
Это половина правды: о). Согласно докам, объект, переданный в deleteLater может быть вообще никогда не удален.
К тому же непонятно что будет если передать туда объект не созданный в куче? Лучше так не делать, это понятно, но как отличить если к вам в функцию пришел указатель на объект и непонятно надо/можно его удалять через deleteLater или нет.
На самом деле я не сильно критикую deleteLater — это хороший инструмент по быстрому исправлению проблем, не более того. Вполне удобное компромиссное решение, по сути костыль, за не имением более достойного варианта.
lexxmark
03.04.2017 21:24Полностью согласен с обоими утверждниями.
Возможно такие указатели были бы полезны только в режиме отладки, как например сделано с итераторами в MS C++, когда указывая нужный DEBUG_LEVEL можно получать кучу ошибок времени исполнения при неправильном использовании итератором.
Когда такое только завезли в MS C++, реально посыпались ошибки в нескольких местах.
А в релизном режиме все превращается обратно в голые быстрые указатели без проверок.
lexxmark
03.04.2017 21:16У shared_ptr есть один неизличимый оверхед — аллокация пямяти для счетчика ссылок. Такое решение в принципе несравнимо с решением без аллокаций.
Satus
03.04.2017 21:19Чем так ужасна аллокация этого счётчика? Не так уж много памяти он занимает.
lexxmark
03.04.2017 21:42если говорить о времени выполнения, то аллокация (хоть одного байта) неизвестно сколько времени будет занимать. Неизвестно как сравнивать с линейным кодом без аллокаций.
Satus
03.04.2017 21:47Ну так эта проблема решается make_shared.
lexxmark
04.04.2017 09:10С ним другие проблемы, написал об этом в комментарии ниже.
Вообще у любого решения есть проблемы и нюансы, в моем они так же присутствуют. От этого никуда не деться. Вопрос лишь баланса ценности и стоимости того или другого интструмента.
kmu1990
03.04.2017 21:30Ну как сказать несравнимо, shared_ptr по крайней мере при удалении последней ссылки не ходит по непонятного размера спику weak_ptr-ов, чтобы их все занулить. А ваш trackable ходит, к тому же еще делает это зачем-то рекурсивно.
lexxmark
03.04.2017 21:39По поводу рекурсии согласен — можно сделать без нее.
Но вот с weak_ptr тоже не все в порядке — недавно была заметка на reddit, о том что weak_ptr не позволяют освободить память под объектом, если объект был создан с помощью make_shared (что активно советуют делать). И если объекты большие то мы получаем почти классический garbage collection — объекта уже нет, а память под ним освободится непонятно когда.kmu1990
03.04.2017 21:511. Советуют или нет использовать make_shared, а голову еще никто не отменял, об этой проблеме известно давно, а даже если бы не было известно, то не трудно догадаться — сюрприза тут нет.
2. Я не говорил, что с weak_ptr нет проблем, мой поинт в том, что вы неправы насчет «несравнимости» подхода с дополнительной динамической аллокацией и без нее.
3. Еще одно подтверждение, что правильный дизайн и голова — лучшие друзья инженера. Умные указатели не освобождают от обязанности понимать, что происходит в коде, в частности, знать кто какими объектами владеет и чем ограничено время жизни объектов, все остальное лишь помошники в этом (возможно даже очень хорошие), но не замена голове.lexxmark
04.04.2017 09:24С этим трудно спорить — светлая голова лучше тёмной: о).
Но светлая голова не панацея. На любого гения найдется такой сложности проект, который окажется ему не под силу. Здесь главное, какие мы даем инструменты, помогающие ему управлять сложностью или выявлять проблемы как можно раньше.
Все мы крепки задним умом, но иногда система, которую мы программируем ведет себя не так как ожидается. Это нормально, в том смысле, что так было, есть и будет. И здесь при прочих равных на первый план выходит качество и мощь как раз инструментов, которыми обладает программист.
В статье описан, как раз один такой инструмент, со своими достоинствами и недостатками. мне кажется баланс достоинств и недостатков вполне приемлем: о)kmu1990
04.04.2017 09:41В статье описан, как раз один такой инструмент, со своими достоинствами и недостатками. мне кажется баланс достоинств и недостатков вполне приемлем: о)
Перфразирую, в стандратной библиотеке уже есть shared_ptr и weak_ptr, которые решают эту проблему. Ваше решение по сравнению с этим выигрывает в отсутствии динамических аллокаций (не известно сколько), проигрывает в алгоритмической сложности «освобождения» (не известно сколько). Я ничего не забыл?lexxmark
04.04.2017 10:15Как указали выше или вот тут, некоторые люди, и я в их числе, не считаем, что shared_ptr каким-то образом предназначены для решения проблем висячих ссылок. Могу если надо найти презентацию Герба с табличкой, в которой перечисленны ситуации и указатели, которые нужно при этом использовать.
Так что боюсь само сравнение не имеет смысла.kmu1990
04.04.2017 10:251. То что они изначально предназначены для разделенного владения не значит, что они не могут решать других задач. Если вас волнует имя, то using вам в помощь, если же вас интересует функциональность, то shared_ptr и weak_ptr обладают нужной функциональностью (по крайней мере я не видел, чтобы кто-то показал обратное).
2. То что Бьерн, сказал, что ситуации когда дизайн программы требует использования shared_ptr из-за непонятного времени жизни программы должны быть сведены к минимуму (что несомненно правда), не значит, что мы не можем использовать shared_ptr/weak_ptr для отлова ошибок в коде.
Нужно слушать не только что кто-то там сказал, но еще и почему кто-то так сказал.
Вы предлагаете отлавливать dangling pointers — это это ошибочная ситуация, weak_ptr и shared_ptr позволяют ее отловить и являются стандартным средством языка. Так что сравнение как раз таки имеет смысл.lexxmark
04.04.2017 21:21если же вас интересует функциональность, то shared_ptr и weak_ptr обладают нужной функциональностью (по крайней мере я не видел, чтобы кто-то показал обратное)
Как я писал в статье, для отлова ошибок, гораздо интереснее момент, когда разрушается объект, на который есть ссылки, а не когда пытаются обратится по невалидному указателю. В этом случае shared_ptr/weak_ptr никак не помогут.
не значит, что мы не можем использовать shared_ptr/weak_ptr для отлова ошибок в коде
Мы не можем просто взять и добавить в существующий код shared_ptr, потому что если ваши объекты лежат на стеке или в unique_ptr или еще где-то, то нельзя их привязать к shared_ptr.
Вернее можно, указав пустой deleter, но смысла в этом нет. Так как weak_ptr отловят окончание использования shared_ptr'ов, а не разрушение объекта.
Вы предлагаете отлавливать dangling pointers — это это ошибочная ситуация, weak_ptr и shared_ptr позволяют ее отловить и являются стандартным средством языка.
Я как раз думаю иначе.
В стандарте нет средств отлова висячих ссылок, эту проблему хотят частично решать статическим анализом. Мне это не совсем нравится, думаю нужно так же runtime решение. Например такое, как у меня.
Так что сравнение как раз таки имеет смысл.
Ну если очень хочется, давайте сравним.
1. Конструирование объекта: shared_ptr — аллокация; track::trackable — ничего.
2. Создание новоро указателя: shared_ptr — инкремент, track::pointer — четыре присваивания указателей.
3. Удаление указателя: shared_ptr — декремент, track::pointer — четыре присваивания указателей.
4. Удаление объекта: shared_ptr — декремент, weak_ptr — деаллокация; track::pointer — проход по списку указателей с вызовом функции on_dangle.
Вроде так.FlexFerrum
04.04.2017 21:52Как я писал в статье, для отлова ошибок, гораздо интереснее момент, когда разрушается объект, на который есть ссылки, а не когда пытаются обратится по невалидному указателю. В этом случае shared_ptr/weak_ptr никак не помогут.
На самом деле, интересно и то, и другое. Факт разрушения объекта, на который есть указатели ничего не говорит о том, где именно они «зависли».
lexxmark
04.04.2017 21:26На самом деле, если не заморачиваться с контейнерами, и перенести чать логики в track::trackable, то можно сделать Track::pointer гораздо эффективнее.
bibmaster
04.04.2017 19:31Есть более дешёвый, но не стандартизованный intrusive_ptr. Но, конечно же без weak указателей. Странно что в boost не заточили на работу с такими указателям семейство intrusive контейнеров. В совокупности они хороши.
Gorthauer87
03.04.2017 21:59+6Чего только люди не изобретают лишь бы не переходить на Rust.
lexxmark
03.04.2017 23:42Зачем в Rust unsafe?
И как раст помогает в этих случаях?
Там черным по белому написано, что иногда Rust слишком консервативен и не позволит скомпилировать код, не поддающийся валидации.
Дело в том, что в С++ сейчас проблема висячих ссылок попадается тоже очень редко. И нет инструмента по по отлову таких случаев.
Уверен в Rust тоже редко возникают «мутные» места, которые нужно оборачивать в unsafe.
И здесь Rust программисту ничем не помогает.TargetSan
04.04.2017 08:54+3С++ — один сплошной unsafe блок. В Rust unsafe чётко локализован и легко поддаётся изоляции и верификации глазками.
lexxmark
04.04.2017 09:29+1Скажите как специалист по Rust, как на нем писать ГУИ. Например, по каким-то событиям удалять часть окон и создавать другие?
И смогу ли я в таком случае случайно удалить кнопку, которая сейчас как раз и рассылает событие клик?TargetSan
04.04.2017 10:19+1Не могу назвать себя спецом по Rust. Я скорее спец по С++, который интересуется Rust.
По поводу UI — не могу ответить. Скорее всего, использовать то что есть через биндинги. Проекты есть, но мне их статус неизвестен.
BlessMaster
04.04.2017 15:54+3Rust — это не тот язык, который запрещает стрелять себе в ногу.
Rust — это тот язык, который даёт инструменты, позволяющие не стрелять (в дополенние к имеющимся в других языках).
Использовать их или нет — это отдельный вопрос, не сводящийся к одному только желанию.
Очевидно, что они в определённых случаях сложнее в использовании и имеют естественные ограничения, происходящие из их природы. Поэтому они — не серебряная пуля.
В Rust — тоже есть инструменты с run-time проверками и прочие источники неожиданностей, к которым нужно относиться соответствующим образом.
«Может ли быть удалена кнопка», которая «сейчас рассылает» (длительный процесс?) и «случайно» ли это произойдёт — это вопрос архитектуры системы. Вопрос в том, чего хочет её разработчик: иметь возможность удалить в произвольный момент времени или держать до последнего, заблокировав обновление интерфейса для других частей системы, пока кто-то ждёт ответа по сети или работает с диском.
Но эта тема пока открыта и ждёт своих героев :-)
kmu1990
04.04.2017 09:33Он локализован, если инженер пишуший код его локализовал. У меня был студент, который на курсе ОС решил писать ОС на Rust, тоже говорил, что Rust такой весь из себя безопасный и тд и тп. Но на практике что я видел, когда проверял его домашние задания:
— проблемы с памятью (с которыми Rust должен помогать по идее)
— unsafe размазанный по коду (с которым по идее должны помагать «правила хорошего тона» Rust-а)
— все те же алгоритмические и логические проблемы (к которым Rust напрямую, конечно, не имеет отношения, но код решающий аналогичную задачу на C был несколько компактнее и проще, ИМХО, именно Rust стал источником многих из этих проблем, как раз из-за переусложнения того, что должно быть простым).
Короче Rust может стать хорошим инструментом только в руках тех, кто умеет им пользоваться (удивительно, но как и любой другой инструмент, включая C++).TargetSan
04.04.2017 10:43+4Начну с конца. Любым инструментом можно как сделать хорошо, так и сделать отвратительно.
По поводу вашего студента — а вы уверены, что на С он бы написал лучше? У меня пока складывается впечатление, что он пытался "вбить молотком" своё решение, не сильно задумываясь о результате. И в С ему бы это удалось гораздо легче.
По моему опять же сугубо личному опыту, Rust провоцирует писать более аккуратный код, более чётко выделять отдельные элементы, более тщательно продумывать, как будут обработаны те или иные нештатные ситуации. Именно из-за своих ограничений, которые не позволяют просто так создать, к примеру, иерархию объектов, которые в реальности ссылаются друг на друга "вверх", "вниз" и "вбок" совершенно произвольно.
По поводу переусложнения — мы всегда платим сложностью в одном месте, чтобы выиграть в другом.
- В С мы выигрываем в простоте самого языка, но платим когнитивной нагрузкой — всю семантику кода приходится тащить в голове.
- В С++ мы платим сложностью языка и сложностью некоторых абстракций за простоту "клиентского" кода — только что-то уж очень часто эти абстракции текут.
- В .NET/Java мы платим сложностью рантайма за более простое написание кода. Который "ломается" менее явно, а потому вызывает иллюзию большей надёжности.
- В Rust мы платим наличием ранее неизвестных концепций и более строгой системой типов за более строгое разделение безопасного и небезопасного кода.
- В том же Haskell мы платим наличием рантайма и непривычной парадигмой за ленивость и отделение побочных эффектов от всего остального кода.
kmu1990
04.04.2017 11:00Начну с конца. Любым инструментом можно как сделать хорошо, так и сделать отвратительно.
Ровно мои слова — Rust такой же инструмент и не надо ему приписывать мифические свойства, которых у него нет. В частности unsafe магическим образом не будет локализован в коде, только потому что вы пишите на Rust.
По поводу вашего студента — а вы уверены, что на С он бы написал лучше? У меня пока складывается впечатление, что он пытался «вбить молотком» своё решение, не сильно задумываясь о результате. И в С ему бы это удалось гораздо легче.
Касательно этого студента, конечно нет. Очевидно, что я бы не стал просить решать задание в двух экземплярах на Rust и на C. Но это лишь демонстрация того, что проблемы никуда не исчезнут магическим образом, потому что Rust.
Но отмечу, что Rust он выбрал добровольно и не сменил своего выбора даже после того, как выяснилось, что сборка bare-metal программ на Rust, который пвроде предназначен для системного программирования, почему-то нормально не описана в документации и вызывает головную боль.
Кроме того я имел возможность посмотреть на других студентов, которые добровольно выбрали C, хотя большими знатоками C или C++ (или вообще дизайна кода) большинство из них не назовешь.
По моему опять же сугубо личному опыту, Rust провоцирует писать более аккуратный код, более чётко выделять отдельные элементы, более тщательно продумывать, как будут обработаны те или иные нештатные ситуации. Именно из-за своих ограничений, которые не позволяют просто так создать, к примеру, иерархию объектов, которые в реальности ссылаются друг на друга «вверх», «вниз» и «вбок» совершенно произвольно.
Студент тоже мне пел такие песни, но результат показал, что это только песни. Моя мысль заключается в том, что не смотря на то, что некоторые люди заявляют о том, что Rust их на что-то там провоцирует, это не значит, что так и есть на самом деле. Я лично, все еще в ожидании хоть каких-нибудь конкретных доводов в пользу безопасности Rust (а не странных утверждений о том, что Rust провоцирует кого-то делать то, что он и так должен был делать).DarkEld3r
04.04.2017 11:49+3В частности unsafe магическим образом не будет локализован в коде, только потому что вы пишите на Rust.
Почему нет? unsafe-блоки всё-таки будут явно выделены. Да, можно весь код в таком блоке написать, но и это имеется очевидное преимущество — сразу видно, что с кодом что-то не так. В плюсах какую-то хрень спрятать, в том числе случайно, проще.
Студент тоже мне пел такие песни, но результат показал, что это только песни.
Ну давайте делать далеко идущие выводы по тому какой код написал один студент.
Я лично, все еще в ожидании хоть каких-нибудь конкретных доводов в пользу безопасности Rust
Каких именно доводов? У языка есть хороший мануал, есть куча статей на эту тему — выводы сделать можно, если есть желание разбираться. Если нет, то о каких доводах можно говорить?
Меня удивляют аргуметы о наличии unsafe: ведь и в C# и в джаве можно сишный код дёргать и нарушить имеющиеся гарантии, но почему-то никто не орёт, что гарантии менеджед языков — миф.
kmu1990
04.04.2017 12:23-1Почему нет? unsafe-блоки всё-таки будут явно выделены. Да, можно весь код в таком блоке написать, но и это имеется очевидное преимущество — сразу видно, что с кодом что-то не так. В плюсах какую-то хрень спрятать, в том числе случайно, проще.
Да можно, а можно и натыкать везде по коду unsafe-ов — все это можно. И от того, что вы используете Rust магическим образом ваше сознание не изменится, вы не станете лучшим инженером и не научитесь грамотно локализовывать опасные места в коде.
Замечу, что не любой C++ или C код делает грязные небезопасные трюки, а код, который так делает все еще можно локализовать в кодовой базе. Да этот код не будет помечен ключевым словом языка, но локализация такого кода — не задача компилятора, а задача инженера. Если вы умеете это делать вы сможете это сделать в C++, если вы не умеете этого делать, вы не сможете этого сделать и в Rust.
Ну давайте делать далеко идущие выводы по тому какой код написал один студент.
А каких выводах речь? Где я делал выводы? Я просто констатирую факт, что аргументация того студента и ваша аргументация идентичны, и его код, не смотря на все эти аргументы, содержал ошибки работы с памятью, ошибки дизайна (unsafe размазанный по коду) и кучу других ошибок.
Каких именно доводов? У языка есть хороший мануал, есть куча статей на эту тему — выводы сделать можно, если есть желание разбираться. Если нет, то о каких доводах можно говорить?
Да о каких выводах вы все говорите? У C++ тоже есть документация и есть статьи и еще больше чем про Rust. Куча этих статей о том как все делать правильно и тд и тп. Чем Rust в этом смысле отличается? Что вообще вы хотели сказать этим комментарием?
Меня удивляют аргуметы о наличии unsafe: ведь и в C# и в джаве можно сишный код дёргать и нарушить имеющиеся гарантии, но почему-то никто не орёт, что гарантии менеджед языков — миф.
Вы похоже забыли, о чем речь. Вы написали, что в коде на Rust все unsafe будут каким-то непонятным образом локализованы. На что я вам отметил, что факт того, что вы пишите на Rust не значит, что unsafe-ы каким-то магическим образом будут локализованы. Вы оспариваете это мое утверждение?
Обращу ваше внимание, что я не говорил, что иметь разделение на safe/unsafe в языке плохо, что программы вызывающие нативный код из managed языков безопасны и тд и тп. Это ваши слова, не мои — не нужно их мне приписывать.TargetSan
04.04.2017 13:24+3Просто чтобы не потерять нить рассуждения.
Вы утверждаете, что Rust ничем не лучше С++, т.к. точно также позволяет при определённом желании наговнокодить, потоптаться по памяти и сделать другие плохие вещи. Более того, он с важей т.з. хуже т.к. накладывает дополнительные ограничения. Также он хуже, поскольку про него меньше статей, best practices etc. Ещё вам, похоже, надоели евангелисты, кричащие на всех углах, как Rust безопасен. Могу понять. Но главное пожалуй то, что для него гораздо меньше наработанного кода, библиотек и поддержки платформ. Если я где-то ошибся, поправьте.
Теперь попробую ответить на ваше утверждение, приведённое в формулировке выше.
- Да, безусловно Rust позволяет сделать какую-нибудь бяку. Потому что язык для системного программирования смотрится мягко говоря странно без возможности прямой работы с памятью. Проблема немного не в том. В С++ требуются определённые усилия чтобы не допустить ошибку. Например, следить, куда и как передаём указатели и ссылки, вовремя смотреть, не "утечёт" ли указатель из вызванной функции в какую-нибудь глобальную переменную и т.п. Да, в тривиальных случаях это не проблема. Но кроме них есть нетривиальные случаи. В Rust при этом дополнительные сознательные усилия требуются чтобы написать неправильно — например, обозначить блок как unsafe, и в нём работать с указателями напрямую. Потому что ссылки даже внутри unsafe проверяются. При этом в документации вполне чётко написано, что unsafe только в случаях, когда вы точно знаете что делаете.
- Да, Rust накладывает дополнительные ограничения. Как раз с целью избежать определённого класса ошибок. Удобно вам такое разделение или нет — вопрос к вам. Безусловно, у каждого свои требования.
- Да, про него значительно меньше информации. Работа в этом направлении ведётся. Как раз на этот год планируется увеличить количество документации, описать основные практики — т.е. сделать язык "ближе к народу". На наработку информации нужно время. Все эти книги по С++ тоже появились не мгновенно в день релиза. Они постепенно появлялись в течение последних 25 лет, вместе с расширением базы пользователей.
- Про евангелизм не буду. Отвечу про "магическую" безопасность. Она не магическая. Это просто устранение довольно широкого класса ошибок работы с ресурсами путём добавления дополнительных статических гарантий. И то, что для отключения этих гарантий требуются дополнительные телодвижения.
- По сути, проблема та же что в пункте 3. Нужно некоторое время. Та же сборка под bare metal постепенно улучшается.
Повторю, если я где-то ошибся в обобщении ваших тезисов — поправьте, я постараюсь ответить.
kmu1990
04.04.2017 16:41Вы утверждаете, что Rust ничем не лучше С++
Покажите мне, где я делал такое утверждение?
т.к. точно также позволяет при определённом желании наговнокодить, потоптаться по памяти и сделать другие плохие вещи.
Да позволяет, но оценку этому я не давал. Указал только на одну конкретную возможность поговнокодить, за которой компилятор не следит (просто потому, что за дизайном кода компилятор не следит).
Более того, он с важей т.з. хуже т.к. накладывает дополнительные ограничения.
Он накладывает дополнительные ограничения, и я видел примеры задач которые на C прекрасно решаются без них. Но оценку, что Rust хуже или лучше (опять же безотносительно области применения) я не давал.
Также он хуже, поскольку про него меньше статей, best practices etc.
Я говорил, что их меньше, но я опять же не говорил, что это делает его хуже или лучше. Более того, замечу что разговор про наличие литературы начал не я, и даже задал вопрос, каким образом это имеет отношение к делу, на который впрочем ответа не получил.
Ещё вам, похоже, надоели евангелисты, кричащие на всех углах, как Rust безопасен.
Такого я не говорил, но это правда (причем нет разницы между евангелистами Rust, C++, Java и даже попадавшимся мне на глаза евангелистом языка Ассемблера), потому что они почему-то очень любят другим приписывать утверждения, которых они не делали.
Но главное пожалуй то, что для него гораздо меньше наработанного кода, библиотек и поддержки платформ.
Это правда, но я об этом не говорил, и уж тем более не мог сказать, что из-за этого Rust плохой.
Если я где-то ошибся, поправьте.
Поправил.
Ответьте мне пожалуйства на следущий вопрос — может быть я правда чего-то не знаю о Rust. Каким образом Rust позволяет локализовать unsafe блоки в коде и не допустить чтобы они разбредались по всей кодовой базе?
Этот вопрос появился не на пустом месте. Потому что, если ваша программа упала и все что вам показывает coredump это покарапченную память. То первыми кандидатами на проверку как раз будут unsafe блоки в Rust (и те части кода, в которых делаются небезопасные вещи в языках где нет unsafe). Если по всей кодовой базе у вас будет торчать unsafe, то при поиске ошибок толку от того, что есть разделение на safe и unsafe код не будет.
Мое утверждение заключается в том, что Rust не помогает это сделать, потому что это вопрос дизайна, а компилятор не следит за дизайном программы. И как пример я приводил моего студента, который, хоть и верил в безопасность Rust и то, что Rust позволит ему сделать код лучше, сделал такой очевидный дизайнерский просчет (в дполонение к другим ляпам).
Исходя из опыта переписки отмечу особенно: я не говорю, что такой проблемы не возникает в C++ (или каком-то другом языке), я не говорю, что эта проблема автоматически и безоговорочно делает Rust плохим и тд и тп. Мое утверждение, что такая проблема не решается запретами.TargetSan
04.04.2017 17:04+3Значит я вас таки неправильно понял. Отвечаю на поставленный вопрос
Ответьте мне пожалуйства на следущий вопрос — может быть я правда чего-то не знаю о Rust. Каким образом Rust позволяет локализовать unsafe блоки в коде и не допустить чтобы они разбредались по всей кодовой базе?
Если коротко — никак. Нет там магии и единорогов. Там есть возможность проверить на границе "безопасности" инварианты и получить дополнительные гарантии, помощь компилятора и проверки внутри безопасной зоны. Но ничто не помешает вам растянуть "границу безопасности" на весь код. Также как в ООП-языке ничто не помешает вам использовать паттерн "Паблик Морозов".
Я, кстати, не видел пока ни одного языка, следящего за дизайном приложения. Есть линтеры — но не встроенные в язык.
kmu1990
04.04.2017 17:14Т. е. вы таки согласны с этим моим утверждением:
Он локализован, если инженер пишуший код его локализовал.
В ответ на ваш комментарий:
С++ — один сплошной unsafe блок. В Rust unsafe чётко локализован и легко поддаётся изоляции и верификации глазками.
Правильно?TargetSan
04.04.2017 17:19+1Да. Согласен. Если код пишет идиот, который считает что компилятор/интерпретатор мешает его творческому полёту мысли, он может написать шлак на чём угодно, начиная с Asm и заканчивая Agda/Idris. Впрочем, до последних он скорее всего не дойдёт.
А где я вам противоречил?
kmu1990
04.04.2017 17:30+11. Не стоит всех, кто делает ошибки в коде называть идиотами. Все когда-то начинали, но это не значит, что все были идиотами.
2. Вы нигде (кроме первого комментария, где вы сказали, что в Rust unsafe четко локализован, с чего все и началось), а вот DarkEld3r, похоже, сомневается в этом.
3. Кроме указания на то, что локализация unsafe-ов магическим образом в коде не появится от того, что код будет написан на Rust я вам тоже не противоречил.DarkEld3r
04.04.2017 17:49+3Вы нигде (кроме первого комментария, где вы сказали, что в Rust unsafe четко локализован, с чего все и началось), а вот DarkEld3r, похоже, сомневается в этом.
Правильнее будет сказать, что (я считаю, что) в расте такие места проще найти и этого достаточно. Ведь нас не волнует абстрактная локализованность? Всегда есть определённые цели. Скажем, если мы ревьюваем код коллеги (или студента), то в С++ придётся внимательно всё просмотреть, а в расте если я увижу сплошные unsafe блоки, то отправлю переделывать. И так пока нормальная локализация не появится. (:
Если я смотрю на код библиотеки, то это тоже вполне может быть показателем того стоит ли её использовать.
Собственно, остаётся только случай когда у человека нет понимания того как надо делать правильно и даже обильные unsafe его на мысль не наталкивают и при этом всём его никто не контролирует. Как по мне, это общая проблема обучения — надо понимать свой инструмент. Разница в том, что в С++ даже если есть понимание как писать надо, всё равно можно забыть проверить (и не важно unique_ptr, shared_ptr или обычный) указатель, а в расте случайно этого сделать нельзя — придётся сознательно написать unsafe. И не надо говорить, что профессионалы таких ошибок не делают — можно посмотреть на статьи о PVS Studio. (:
TargetSan
04.04.2017 19:44+4- Не стоит, это правда. Но новички будут ошибаться просто от незнания инструмента. В список таких ошибок входит и неправильный дизайн. Но мы вроде говорим об инженерах, т.е. людях, которые хотя бы главу про unsafe прочтут в доке.
- Неправильная формулировка с моей стороны. Правильная — "В Rust небезопасное поведение локализовано в unsafe блоках"
- Хм, я вроде нигде не говорил, что unsafe сам магически локализуется.
DarkEld3r
04.04.2017 14:07+3И от того, что вы используете Rust магическим образом ваше сознание не изменится
Дык, я такого и не утверждал. Хочу сказать, что мне проще работать с кодом в котором "опасные места" выделены явно. Причём под "работать" подразумевается и чтение и правка и, что не менее важно, ревью изменений других разработчиков.
На одном из плюсовых проектов, над которым работал, у нас была специальная тулза, которая следила за соблюдением определённых правил. И в какой-то момент оказалось, что кое-что на самом деле не проверялось (баг в тулзе). И после его исправления обнаружилось, что нарушения были. И это при том, что команда весьма сильная и ревью обязательно (минимум двумя людьми). Всё потому что глаз замыливается, а изменения бывают большие и сложные.
Это я к тому, что пользу от unsafe вижу не только и не столько в "удоблетворении компилятора", а в том, что это маркер. Да, можно им не пользоваться и да, "идеальные разработчики" ошибок не делают. Вот только в последних не верю, а наличие инструмента, который можно использовать неправильно (или не использовать вообще), лучше, чем полное его отсутствие.
Если вы умеете это делать вы сможете это сделать в C++, если вы не умеете этого делать, вы не сможете этого сделать и в Rust.
"Немного" не так. Есть ключевое отличие: в расте из коробки есть формальные метрики. И если я вижу unsafe, то начинаю смотреть на код внимательнее. Если unsafe слишком много — вполне себе повод отправить на переделку.
аргументация того студента и ваша аргументация идентичны
А какая у меня аргументация? Да, я считаю, что у раста есть ряд преимуществ (как и недостатков, впрочем), но приводить их, в очередной раз, смысла не вижу. Просто сделал комментарий конкретно про unsafe и на эту тему готов поспорить.
Что вообще вы хотели сказать этим комментарием?
Хочу сказать, что вопрос "какие у раста преимущества" провокационный. Вот как на это можно ответить? Если вкратце, то получатся сплошная агитация без фактов, если развёрнуто, то в комментарий можно не поместиться.
kmu1990
04.04.2017 17:01«Немного» не так. Есть ключевое отличие: в расте из коробки есть формальные метрики. И если я вижу unsafe, то начинаю смотреть на код внимательнее. Если unsafe слишком много — вполне себе повод отправить на переделку.
Согласен, Rust не даст вам опустить unsafe и если кто-то кроме вас смотрит код и он достаточно хорошо разбирается в дизайне, то этот кто-то сможет вам на это указать.
Но продумать дизайн кода, так что бы unsafe не распространялся он за вас не сможет. И в этом мой поинт — если вы умете это делать, вы сможете это сделать и в C++ и в Rust, если нет, то вы не сможете этого сделать ни там ни там.
А какая у меня аргументация? Да, я считаю, что у раста есть ряд преимуществ (как и недостатков, впрочем), но приводить их, в очередной раз, смысла не вижу. Просто сделал комментарий конкретно про unsafe и на эту тему готов поспорить.
Прошу прощения, я не посмотрел на ник и перепутал вас с другим оппонентом и о его аргументации (цитата была в комментарии выше) я и говорил.
Хочу сказать, что вопрос «какие у раста преимущества» провокационный. Вот как на это можно ответить? Если вкратце, то получатся сплошная агитация без фактов, если развёрнуто, то в комментарий можно не поместиться.
Я такого вопроса не задавал. Я спросил, каким образом наличие или отсутствие литературы относится к делу. А объективных доводов пользу безопасности Rust я все еще не могу увидеть, основные причины:
— я видел код написанный на Rust, при этом имеющий проблемы работы с памятью, т. е. Rust не делает магии — хороший инженер будет хорошо писать на Rust (С++/Java/Haskell), плохой везде будет писать плохо (несколько слишком суровое обобщение, но оно должно донести суть идеи)
— кроме проблем работы с памятью есть и другие проблемы связанные с дизайном, логикой внешними зависимостями и Rust тут не помошник
— для борьбы с проблемами неправильного использования памяти существует некоторое количество разнообразных автоматических средств (тоже самое относится к проблемам связанным с конкуретностью)
На сколько, с учетом всего этого, переход на Rust при прочих равных условиях сделает жизнь лучше? Если хоть какой-то способ показать, что Rust реально улучшает качество? Когда я задаю такие вопросы, то получаю вот такие странные ответы (это реальная цитата из моей переписки про Rust):
Мне субъективно приятней писать на расте, чем на C++, благодаря красивому и лаконичному синтаксису, крутой системе типов, единообразию кодовой базы, отсутствию неявных преобразований, аккуратности во всем (например, гигиеничные макросы), крутым современным фишкам вроде классного паттерн-матчинга, очень хорошей системе сборки, expression-ориентированности, шикарному выведению типов, наличию большого числа статических проверок, ну и ОЩУЩЕНИЮ безопасности.
DarkEld3r
04.04.2017 17:35+2Но продумать дизайн кода, так что бы unsafe не распространялся он за вас не сможет.
Не совсем понимаю, если честно. Вообще, кривая освоения языка у раста не самая плавная, но С++ на "нормальном" (а не базовом) уровне освоить тоже не просто. Собственно, я язык воспринимаю именно в разрезе когда у нас есть и опытные разработчики и новички — в таком случае первым будет проще разобраться с кодом вторых и объяснить что к чему, в том числе и в плане дизайна, ведь этому тоже надо учиться.
Кстати, можно вообще сделать запрет unsafe кода и компилятор за этим будет следить, что тоже полезно в плане разделения зон ответственности.
Я спросил, каким образом наличие или отсутствие литературы относится к делу.
Про литературу — это я опять же на вопрос о преимуществах языка отвечал, в общем случилось недопонимание.
я видел код написанный на Rust, при этом имеющий проблемы работы с памятью
А я видел "утечки памяти" в менеджед языках — значит GC "не делает магии" (и не нужен)?
Или иначе: зачем нам всякие
unique_ptr
, если новичок может продолжать использовать явныеnew
?
кроме проблем работы с памятью есть и другие проблемы связанные с дизайном, логикой внешними зависимостями и Rust тут не помошник
Аналогично. Не понимаю этот аргумент: да, решается определённая задача, а не "сделать хорошо", разве это значит, что она не нужна? Я бы ещё понял, если бы речь шла о том, что налогаемые ограничения нивелируют эти самые гарантии — тут ещё есть о чём поговорить, хотя опять же, легко скатиться в субъективщину.
На сколько, с учетом всего этого, переход на Rust при прочих равных условиях сделает жизнь лучше? Если хоть какой-то способ показать, что Rust реально улучшает качество? Когда я задаю такие вопросы, то получаю вот такие странные ответы
Так вопрос тоже странный. Ведь "сделать жизнь лучше" можно очень по разному, совсем не только увеличением "безопасности и качества". Собственно, преимущества от языка и по моему мнению не исчерпываются дополнительными гарантиями. Если же всё-таки говорить о последних, то на мой взгляд, разница следующая: в С++ надо делать дополнительные телодвижения, чтобы получить безопасность, в расте — наоборот. Опять же, наступить на грабли UB сложнее.
Относительно того даст ли что-то "переход" — тут всё зависит от этих самых "прочих равных". На прошлом проекте у нас на старте была, по сути, сформировавшаяся команда, которая знала С++ (и делала на нём похожий проект), активно использовались библиотеки, ну и существовал ещё ряд нюансов из-за которых даже я, как человек, которому раст симпатичен, не стал бы предлагать его использовать. На текущем проекте требования совсем другие: сравнительно небольшое ядро, не такая большая зависимость от библиотек и т.д. — раст использовать возможно, что и было сделано.
Может я так и не понял вопрос, но пару раз прозвучало про "отсутствие магии" — да, именно так и есть. Язык делает не больше того, что обещает. Если этого кажется мало, ну что ж, каждому предстоит решать самостоятельно.
kmu1990
04.04.2017 18:07Не совсем понимаю, если честно. Вообще, кривая освоения языка у раста не самая плавная, но С++ на «нормальном» (а не базовом) уровне освоить тоже не просто.
Дело не в кривой освоения языка, дело в дизайне. Разделении на сущности, распределении обязанностей в коде, и тд и тп — это дизайн.
Кстати, можно вообще сделать запрет unsafe кода и компилятор за этим будет следить, что тоже полезно в плане разделения зон ответственности.
Условно вы получите Java, там не супер часто используются нативные binding-ы (за исключением таковых в стандартной библиотеке), и вот вам безопасное окружение (при условии, что мы доверяеме JVM и стандартной библиотеке).
Но язык предназначенный для системного программирования, в частности, должен уметь решать задачи в которых без unsafe не обойтись (например, попробуйте на Rust написать динамический универасльный аллокатор памяти без unsafe). На сколько я понимаю Rust относит себя как раз к языкам для системного программирования.
Про литературу — это я опять же на вопрос о преимуществах языка отвечал, в общем случилось недопонимание.
Я задал вопрос о конкретных доводах в пользу безопасности Rust. На сколько реально улучшается качество кода при использовании Rust (я могу поверить, что если вы никогда не используете unsafe он достаточно безопасен для многих задач)? Потому что заявлений о безопасности Rust я слышал много (причем в виде Rust безопасен и точка), впрочем на сайте они прямым текстом пишут, что «Rust is totally a safe programming language» но только до тех пор пока вы не начнете использовать unsafe.
Но простите, есть класс задач, в которых приходится использовать unsafe и отсюда возникает вопрос, а на сколько же реально он «safe» в задачах системного программирования? Мое мнение такое, в задачах системного программирования, он безопасен ровно на столько, на сколько хорошо продуман и задизайнен ваш код. Но как не трудно заметить это же касается и C и C++, и на самом деле любого языка (хотя, справедливости ради, тут нужно вспомнить мнение одного товарища, что C++ ведет к реально плохим дизайнерским решениям).
Так вопрос тоже странный. Ведь «сделать жизнь лучше» можно очень по разному, совсем не только увеличением «безопасности и качества». Собственно, преимущества от языка и по моему мнению не исчерпываются дополнительными гарантиями. Если же всё-таки говорить о последних, то на мой взгляд, разница следующая: в С++ надо делать дополнительные телодвижения, чтобы получить безопасность, в расте — наоборот. Опять же, наступить на грабли UB сложнее.
Перефразирую. С учетом того, что есть классы ошибок с которыми Rust не поможет справиться, с учетом того, что есть средства для поиска ошибок работы с памятью для других языков, с учетом того, что Rust это новый язык с новыми сложностями. На сколько это оправдано начинать проект на Rust, чтобы повысить «безопасность»?
BTW, я посмотрел на список того, что считается UB в Rust, он не сильно то отличается от того, что считается UB, например, в C.DarkEld3r
04.04.2017 18:28+3Дело не в кривой освоения языка, дело в дизайне.
Дык, умение "правильно дизайнить", в какой-то степени, зависит и от языка. В любом случае, начали мы с того, что опытный товаришь не сможет "продумать дизайн" за меня. Так почему нет? Да, вначале новичкам придётся подробно говорить как делать, ну так это обучение и есть.
например, попробуйте на Rust написать динамический универасльный аллокатор памяти без unsafe
Так я об этом и говорю: более опытные разработчики пишут такие штуки, а менее опытные — используют. И в модулях последних unsafe можно и запретить. Получаем автоматический контроль за тем, чтобы небезопасные штуки не расползались по коду.
Мое мнение такое, в задачах системного программирования, он безопасен ровно на столько, на сколько хорошо продуман и задизайнен ваш код.
Могу только повторить аргумент об автоматической проверке. Можно идеально продумать/задизайнить, а потом кто-то воткнёт reinterpret_cast (или лучше — сишный каст) и на ревью обо пройдёт незамеченным. Или забудет проверить нулевой указатель и т.д. Всё это не имеет отношения к дизайну и отлавливается растом.
На сколько это оправдано начинать проект на Rust, чтобы повысить «безопасность»?
Ответ всё тот же: зависит от проекта. Универсальных решений не бывает.
BTW, я посмотрел на список того, что считается UB в Rust, он не сильно то отличается от того, что считается UB, например, в C.
Да ладно? Можно взглянуть на список?
Подозреваю, что речь идёт о том, что можно сделать в
unsafe
: тогда мы опять возвращаемся к тому, что в С/С++ код является сплошным unsafe-блоком, а в расте такие блоки можно (и нужно) изолировать.kmu1990
04.04.2017 18:41Дык, умение «правильно дизайнить», в какой-то степени, зависит и от языка. В любом случае, начали мы с того, что опытный товаришь не сможет «продумать дизайн» за меня.
Я не говорил такого, я говорил, что Rust не сможет придумать за вас и вам все еще нужен опытный товарищь.
А после того, как вы перешли из разряда новичков, на сколько Rust вам упрощает жизнь и повышает качество вашего кода? Потому как, если мы говорим уже не о новичках, то и в C++ не новичкам не так чтобы очень часто приходится сталкиваться с проблемами с памятью, отчасти потому что эти темы довольно неплохо проработаны и есть средства борьбы с проблемами.
Так я об этом и говорю: более опытные разработчики пишут такие штуки, а менее опытные — используют. И в модулях последних unsafe можно и запретить. Получаем автоматический контроль за тем, чтобы небезопасные штуки не расползались по коду.
Если за вашей работой все время кто-то следит, то запрещать можно и в C++, разница в том, что компиялтор не может следить за этими запретами (если вы не напряглись и не стали использовать какую-то тулу, компиляторный плагин или еще что-нибудь, тут опять же довольно много используемых на практике вариантов). В этом разница есть, на сколько она существенна вопрос.
Могу только повторить аргумент об автоматической проверке. Можно идеально продумать/задизайнить, а потом кто-то воткнёт reinterpret_cast (или лучше — сишный каст) и на ревью обо пройдёт незамеченным. Или забудет проверить нулевой указатель и т.д. Всё это не имеет отношения к дизайну и отлавливается растом.
А слово unsafe какое-то магическое и оно никогда не пройдет на ревью незамеченным?
BTW, как вы себе представляете такой запрет? Компилятор будет спрашивать логин и пароль, если пользователь наовчиек, то одни флаги компиляции, если нет то другие? Потому как на ревью можно это проглядеть так же как и в любом другом языке.
Ответ всё тот же: зависит от проекта. Универсальных решений не бывает.
Абсолютно согласен и эту мысль пытаюсь донести до всех, кто считает, что может просто забыть про новые проекты на C и C++ и заменить их Rust-ом, потому что «безопасно».
Да ладно? Можно взглянуть на список?
Смотрите https://doc.rust-lang.org/reference.html#behavior-considered-undefinedDarkEld3r
04.04.2017 19:32+2то и в C++ не новичкам не так чтобы очень часто приходится сталкиваться с проблемами с памятью
Люди склонны ошибаться. В любом случае, мы переходим в область субъективных суждений: вряд ли, существует статистика как часто такие ошибки допускают. Могу снова предложить почитать статьи авторов PVS Studio и сделать поправку на то, что им попадает в руки код после ревью, прохождения тестов, а бывает, что и после других анализаторов.
И да, всё что делает раст — "встраивает" некоторые проверки в сам язык без необходимости подключать тяжёлую артиллерию в виде статических анализаторов и валгринда (и да, я в курсе, что эти инструменты могут быть полезны и в расте).
Кстати, отсутствие дата рейсов тоже гарантируется.
разница в том, что компиялтор не может следить за этими запретами
Я изначально именно это и утверждал. Отсутствие автоматической проверки сразу обесценивает большинство "договорённостей".
А слово unsafe какое-то магическое и оно никогда не пройдет на ревью незамеченным?
Следить за отсутствием одного ключевого слова гораздо проще, чем за кучей хитрых нюансов языка. И да, уже есть инструменты для этого, в том числе боты для гитхаба, которые говорят "внимание, в этом ревью есть unsafe код!".
BTW, как вы себе представляете такой запрет?
Например, новичкам нельзя лезть в модуль
core
, а в остальных запрет включён и да, компилятор там не разрешитunsafe
.
Смотрите https://doc.rust-lang.org/reference.html#behavior-considered-undefined
Посмотрел, перечислены штуки, которые можно сделать только в unsafe, что и требовалось доказать.
kmu1990
04.04.2017 19:53Люди склонны ошибаться. В любом случае, мы переходим в область субъективных суждений: вряд ли, существует статистика как часто такие ошибки допускают. Могу снова предложить почитать статьи авторов PVS Studio и сделать поправку на то, что им попадает в руки код после ревью, прохождения тестов, а бывает, что и после других анализаторов.
И Rust это никак не исправляет, вводит новые сложности, которые устраняют возможность каких-то ошибок при определенных условиях, но на сколько эти ошибки влияют на качество проекта в целом, сколько новых ошибок будет из-за сложностей самого Rust-а — вопросы, которые остаются без ответа.
Кстати, отсутствие дата рейсов тоже гарантируется.
Только для ограниченного набора многопоточных примитивов. Потому как только вам нужно залезть в нетривиальный lock-free, реализовать SMR и тд, вам тут же приходиться использовать unsafe.
Посмотрел, перечислены штуки, которые можно сделать только в unsafe, что и требовалось доказать.
Да они unsafe и что дальше? Для системного программирования приходиться использовать unsafe, или вы предлагаете новичкам вообще никогда не лезть в системное программирование? В таком случае предлагаю не называть Rust языком для системного программирования.TargetSan
04.04.2017 20:43+1Вполне возможно, вы найдёте ответ на свой вопрос по этой ссылке:
https://github.com/redox-os/redox
Да, это пока исследовательский проект. Но эта ОС вполне себе работает. Ядро написано на голом Rust.kmu1990
04.04.2017 21:08-2Это просто несерьезно… Когда redox будет использоваться кем-то кроме его создателей и для чего-то кроме развлечений, тогда можно будет сказать, что Rust показал себя в мире системного программирования. Да и вообще называть исследовательской ОС, вся новизна которой в том, что она написана на Rust — слишком громко.
Даже элементарно, ребята в ядре используют топрный free-list алокатор с кучей ограниченной 256MB и эта константа забита в коде к тому же залоченный одним общим мьютексом. И это при том, что аллокация памяти довольно разнообразная на всякие интересные, быстрые и простые алгоритмы. Пожалуй перед тем как браться за ОС им стоит элементарно познакомиться с тем, что было сделано другими (серьезно, например, SLAB аллокаторы появились в SunOS, т. е. это не пипец какой bleeding edge, уж не говоря о том, что алгоритм там элементарный).
Но на самом деле системное программирование не ограничивается только ядром ОС и писать полноценную ОС для этого не обязательно.
Или вы этим комментарием хотели показать, что там нет unsafe? Там он есть, я это знаю наверняка.TargetSan
04.04.2017 21:48+4А теперь объясните, чем для вас плох этот пример.
На Rust? На Rust.
Системный код? Системный код.
Ансэйф изолирован? Изолирован, я смотрел.
Или вам подойдёт только Unreal Engine, написанный на Rust для квантового компьютера и имеющий пользовательскую базу не менее 10 миллионов человек?
kmu1990
04.04.2017 22:18-2Эта ОС примитивный конкурентный код, где все защищено одним или несколькими несколькими глобальными локами (я даже не говорю про какие-то там lock-free, вопрос в элементарной конкуретности), а аллокатор памяти в этой ОС просто шутка. Этот код просто утрированный пример.
Серьезно, у меня некоторые студенты на 2-3 курсе писали более содержательный системный код, разве что я им не давал задания прикручивать к своему коду UI-чик, так как есть несколько более базовые задачи.
Но проблема не только в этом. То что этот код не начали использовать в реальных продуктах, значит, что одних гарантий безопасности Rust-а не достаточно (возможно пока), чтобы перейти на него для системной разработки. Я молчу о том, чтобы получить хоть какую-то статистику об ошибках и надежности года на Rust в реальных проектах. И именно поэтому я указал вам на то, что этот код никто кроме разработчиков для реальных задач не использует.
И нет, мне не нужен Unreal Engine, уж точно не как показатель применимости кода для системного программирования. Совершенно точно квантовые компьютеры не имеют никакого отношения к делу. И о пользовательской базе в 10 миллионов человек я не говорил. И я еще даже не упоминал про такие вещи как QNX и L4* (которые создавались, в том числе, с расчетом на безопасность). Если вы перестанете утрировать, а перейдете к объективным аргументам, то обсуждение будет гораздо полезнее.TargetSan
04.04.2017 22:34+4Окей. Тогда, чтобы я не утрировал, приведите пожалуйста критерии кода, который будете считать достаточным аргументом.
kmu1990
04.04.2017 22:47-3А вы прочитали комментарий? Там написано, про код используемый в реальных продуктах. Но да спасибо низачто, за то время пока вы ерничали я успел найти реальный код используемый в продакшене и написанный на Rust.
DarkEld3r
05.04.2017 10:01+1И Rust это никак не исправляет,
Нет, (определённые ошибки) исправляет. Банально я не смогу забыть проверить указатель/
optional
.
По какому это уже кругу мы пошли?..
сколько новых ошибок будет из-за сложностей самого Rust-а — вопросы, которые остаются без ответа.
Ага.
Мне просто интересно каким мог бы быть ответ, чтобы вы его "засчитали"? Любой опыт из практики будет частным случаем, а универсальных метрик не изобрели, да и вряд ли они вообще возможны.
Ну и я могу сказать, что другие "мелкие приятности" в языке тоже от ошибок защищают. Например, нельзя забыть
break
вswitch
, нет автоматического приведения типов и т.д. Разумеется, метрик снова нет, так что отвечать "глубокомысленным" комментарием про вопросы без ответов не обязательно.
вам тут же приходиться использовать unsafe.
И что? Если unsafe можно обернуть в безопасный интерфейс, то проблемы нет. Точно так же, как в С++ мы доверяем стандартной (и не только) библиотеке, так и в расте. С той разницей, что используя безопасный интерфейс выстрелить в ногу уже нельзя.
Для системного программирования приходиться использовать unsafe
Для системного программирования unsafe можно точно так же изолировать.
lexxmark
04.04.2017 23:28+1Люди склонны ошибаться. В любом случае, мы переходим в область субъективных суждений: вряд ли, существует статистика как часто такие ошибки допускают. Могу снова предложить почитать статьи авторов PVS Studio и сделать поправку на то, что им попадает в руки код после ревью, прохождения тестов, а бывает, что и после других анализаторов.
Соглашусь. Rust в этом смысле шаг вперед. Особенно если посмотреть на историю его создания.
И да, всё что делает раст — «встраивает» некоторые проверки в сам язык без необходимости подключать тяжёлую артиллерию в виде статических анализаторов и валгринда (и да, я в курсе, что эти инструменты могут быть полезны и в расте).
Может языку С++ стоит идти в эту же сторону, но с другой стороны — вставлять куски кода помеченные как safe, где будут работать строгие проверки как в Rust. И мы в своем коде будем понемногу расширять область safe: о)TargetSan
04.04.2017 23:34+2Не выйдет. Семантика С++ конфликтует с такими проверками.
lexxmark
05.04.2017 10:16Не понятно.
В Rustе можно иметь две семантики, а в С++ нельзя?TargetSan
05.04.2017 10:54+3В Rust одна семантика.
Что конфликтует с лайфтаймами в С++ — принцип "копирование первично". Т.е. даже после перемещения объекта на его месте остаётся "заглушка" в неопределённом, пусть и валидном состоянии.Проконтролировать такое можно в простых случаях, с менее простыми будут проблемы.FlexFerrum
05.04.2017 10:59Эм… Разве сейчас именно такой принцип? Всё, что я об этом слышал, говорит скорее об обратном — копирование выполняется только в том случае, если нельзя выполнить перемещение. И, хоть это и не декларируется явно стандартом, но в целом требуется, чтобы после перемещения исходный объект оказался в валидном пустом состоянии.
TargetSan
05.04.2017 14:36+3Дело именно в этом. Семантически такой "пустой объект" — всё ещё объект. К нему можно получить доступ, дёргать методы и т.п. Более того, С++ позволяет легко сделать ссылку-перемещение из простой мутабельной ссылки. В ряде случаев это может приводить к, например, неконтроллируемому появлению ссылок на элементы вектора, к примеру, которых уже давно нет на этом месте. Потому что исходный вектор был мало того что перемещён, а вдобавок реаллоцирован. В Rust такой перемещённый объект становится недоступен в принципе.
FlexFerrum
05.04.2017 23:03+1А вот с этого места по-подробнее. То, что после move'а объект должен переходить в состояние «инициализирован конструктором по-умолчанию» — это, в целом, логично и вписывается в идеологию языка. Ведь для объекта когда-нибудь будет позван деструктор. И точку вызова деструктора компилятор предсказать не может. Поэтому полностью разрушать объект в точке трансфера состояния — нельзя. А вот в каких сценариях появляются, как вы пишете, «неконтролируемые ссылки на элементы вектора» — мне очень интересно. Хотя бы один пример приведите, пожалуйста.
DarkEld3r
05.04.2017 10:20+2Может языку С++ стоит идти в эту же сторону, но с другой стороны — вставлять куски кода помеченные как safe
Какие-то подвижки в эту сторону уже идут: в C++ Core Guidelines вводятся новые типы (потенциально) с дополнительными проверками.
Собственно, я хоть и защищаю раст, но мне не так принципиален "взлёт" именно этого языка, а скорее попадание его идей "в мейнстрим". Хотя, конечно, в новом языке их можно удачнее интегрировать, а не приделывать сбоку.
mellinare
04.04.2017 23:06+4Да этот код не будет помечен ключевым словом языка, но локализация такого кода — не задача компилятора, а задача инженера. Если вы умеете это делать вы сможете это сделать в C++, если вы не умеете этого делать, вы не сможете этого сделать и в Rust.
Вы похоже забыли, о чем речь. Вы написали, что в коде на Rust все unsafe будут каким-то непонятным образом локализованы
Зачем инженеру решать задачу (к тому же, весьма непростую), если есть автоматические способы ее решения? Компилятор Rust выдаст ошибку компиляции, если опасный, по его мнению, участок кода не будет обернут в unsafe блок. Если программист в своем уме, то тыкать unsafe тут и там просто так он не будет. Таким, вполе понятным образом, unsafe блоки будут локализованы. Если вы не решаете какую-то низкоуровневую задачу (написание ОС\аллокатора\структуры данных), то, скорее всего, unsafe вам вообще не понадобится.
Собственно, к чему вообще Rust был упомянут в комментариях: там проблема висячих указателей решается на этапе компиляции (с помощью lifetime).lexxmark
04.04.2017 23:23+2Да. Rust хорош, особенно в воспитательных целях. Вместо преподавателя, по рукам дает компилятор.
Собственно, к чему вообще Rust был упомянут в комментариях: там проблема висячих указателей решается на этапе компиляции (с помощью lifetime).
На что был ответ — не всегда пожно перейти на Rust, поэтому комментарий про Rust вообще не в тему.DarkEld3r
05.04.2017 10:43+3не всегда пожно перейти на Rust, поэтому комментарий про Rust вообще не в тему.
Я бы даже сказал, что "перейти" (если речь не об одном человеке) можно в исключительных случаях, но на опыт "конкурентов" посмотреть всё равно бывает полезно.
kmu1990
04.04.2017 23:23-2Под локалаизцией я понимаю не только то, что код будет обернут в unsafe блок (как это вообще можно называть локализацией?). Локализация, это когда ваш unsafe код сведен к минимуму и не размазан по всему коду. Как Rust поможет автоматически вам решить эту задачу?
Если вы не решаете какую-то низкоуровневую задачу (написание ОС\аллокатора\структуры данных), то, скорее всего, unsafe вам вообще не понадобится.
Простите, но в системном программировании примерно такие задачи и решаются.
FlexFerrum
04.04.2017 10:48+1Хм… Ваша статья написана несколько сумбурно (особенно вступительная часть), поэтому суть проблемы и решение понял, только прочитав статью дважды. И то не факт, что понял до конца. Теперь по сути. Как бы это странно не звучало, но лучший способ борьбы с dangling pointers — это не использовать «голые» указатели в коде вообще. Как, собственно, и рекомендуется в Core Guidelines. И да, действительно, в данном случае статический анализ + некоторая поддержка в рантайме может помочь больше, чем предлагаемые trackable-поинтеры. В том же GSL сейчас есть шаблонный класс owner, который просто показывает (статическому анализатору!) кто владеет объектом. Есть класс not_null, который гарантирует контракт, что хранимый в нём указатель гарантированно не будет nullptr. По такому же принципу можно добавить класс scoped_pointer, который будет гарантировать, что любая его копия умрёт раньше, чем указатель, из которого она создана и которым «временно владеет». Нарисовать такой класс, поддерживающий разные ситуации (но, очевидно, не все — инвалидирующиеся итераторы всё равно будут портить жизнь) — довольно интересная задачка.
lexxmark
04.04.2017 21:54Спасибо за развернутый комментарий.
это не использовать «голые» указатели в коде вообще.
Так вот нет невладеющих смарт указателей в стандарте (есть попытка observer_ptr).
owner и not_null хороши, но мне кажется not_null это полиси к любому другому указателю, а не отдельный тип.
чтобы можно было иметь owner_not_null, unique_ptr_not_null, shared_ptr_not_null и т.п.
По такому же принципу можно добавить класс scoped_pointer, который будет гарантировать, что любая его копия умрёт раньше, чем указатель, из которого она создана и которым «временно владеет».
очень интересная задачка. есть мысли как можно реализовать эти гарантии? запретить копирование/перемещение?FlexFerrum
04.04.2017 22:09+1owner и not_null хороши, но мне кажется not_null это полиси к любому другому указателю, а не отдельный тип.
not_null можно инициализировать любым указателем (и обычным, и smart).
очень интересная задачка. есть мысли как можно реализовать эти гарантии? запретить копирование/перемещение?
Ну, по зрелому размышлению это трансформировалось немного в другое — в safe_ptr, который «стреляет» при доступе к разрушенному указателю или при попытке разрушить объект, на который ещё висят указатели. Идея (как я сейчас посмотрел по коду) очень близка к вашему trackable, только без разделения на отдельный trackable-класс и pointer. То есть если объект конструируется из указателя (одного из вариантов) — создаётся специфичный для указателя trackable. Если конструктором копий — то pointer. Всё это за одним фасадом. Список экземпляров, «вдалеющих» trackable, по большому счёту, имеет смысл только для debug-режима, чтобы иметь возможность отследить, где был совершён доступ к разрушенному указателю. На самом деле, для этого даже простой «скрытой» связки shared_ptr + weak_ptr хватит (ловить то надо факт лочки мёртвого shared_ptr). А для релиза достаточно счётчика ссылок и проверки, что при разрушении trackable счётчик равен нулю. Оповещать указатели о том, что объект сдох — бессмысленно. Это априори ошибочная ситуация, которая в «идеальном случае» должна закончиться AV.lexxmark
04.04.2017 23:17смотрю на safe_ptr.
да, прям мои мысли, когда я начинал писать свой веловипед. Потом захотелось прикрутить стандартные контейнеры и пришлось всё усложнить. Спасибо за ссылку.
ловить то надо факт лочки мёртвого shared_ptr
гораздо интересенее момент разрушения объекта, на который есть ссылки.
А для релиза достаточно счётчика ссылок и проверки, что при разрушении trackable счётчик равен нулю. Оповещать указатели о том, что объект сдох — бессмысленно.
согласен, но у меня не получилось прикрутить такой простой указатель к стандартным контейнерам. когда на все элементы контейнера один trackable в аллокаторе. Может есть мысли как это можно улучшить?lexxmark
04.04.2017 23:40кстати safe_ptr не поддерживают указатели на вложенные объекты (например shared_ptr может), это еще одна причина, почему он такой простой. ну и поддержки многопоточности нет, как и у меня.
FlexFerrum
05.04.2017 00:13Что именно имеется в виду?
FlexFerrum
05.04.2017 00:13смотрю на safe_ptr.
да, прям мои мысли, когда я начинал писать свой веловипед. Потом захотелось прикрутить стандартные контейнеры и пришлось всё усложнить. Спасибо за ссылку.
Но это не тот safe_ptr, который я имел в виду. Это какой-то интрузивный смарт-поинтер со списком владения. Свой я сейчас набросаю.
гораздо интересенее момент разрушения объекта, на который есть ссылки.
По моему опыту отладки программ с большим количеством объектов с интрузивным счётчиком ссылок скажу так: гораздо важнее знать, то, какой объект «зажал» лишнюю ссылку, чем то, на что именно ссылка подвисла. Особенно если речь об объектах с временем жизни сопоставимым со временем жизни всей программы.
согласен, но у меня не получилось прикрутить такой простой указатель к стандартным контейнерам. когда на все элементы контейнера один trackable в аллокаторе. Может есть мысли как это можно улучшить?
Вот, кстати, фишка с контейнерами из статьи вообще не ясна. Что именно и к чему хочется прикрутить? Какой сценарий?lexxmark
05.04.2017 08:37Вот, кстати, фишка с контейнерами из статьи вообще не ясна. Что именно и к чему хочется прикрутить?
Хочется использовать такие указатели с элементами контейнеров. То есть не касаясь типа хранимого в контейнере, иметь возможность следить за временем жизни элементов — по сути это решение проблем с инвалидацией итераторов.
Какой сценарий?
Ну например какой-то кеш, который может инвалидороваться:
std::map<key, cached_value_type> cache; cached_value_type* get_value(key) { // создать кеш значение для ключа, если его еще нет ... return cache[key]; }
да и везде, где мы передаем элемент контейнера вовне.
Заменяем std::map на rsl::track::map и тип возвращаемого указателя с cached_value_type* на rsl::track::pointer<cached_value_type>, и вуаля — при инвалидации или удалении кеша мы ловим повисшие ссылки на него.FlexFerrum
05.04.2017 09:56+1С контейнерами все грустно. Как мне кажется, вы пытаетесь предоставить гарантий больше, чем у вас вообще есть. То, что у вас trackable привязан к аллокатору, а не к аллоцированному объекту делает его фактически бесполезным. map и list аллоцируют память под каждый элемент. Вектор — пачками. Дека — чанками. При этом все они имеют массу оптимизаций, из-за которых отследить фактическую инвалидацию элемента в любых случаях — невозможно. Вы вот в тестах на vector проверяете, что при erase хвостовые указатели инвалидируются, а не то, что объект, на который указывает удалённый указатель, становится невалидным. Согласитесь, это разные вещи. Код, в котором есть указатель на удалённый объект, может быть очень "удивлён", что значение объекта внезапно сменилось. Это допустимо с голыми указателями, но такого не ожидаешь от смарт-указателей которые, вроде как, от этого гарантируют. Поэтому да, в моем представлении время жизни trackable-объекта должно быть всегда меньше, чем того объекта, на который он указывает. И не важно — на стеке этот объект, в контейнере или в динамической памяти.
lexxmark
05.04.2017 10:38из-за которых отследить фактическую инвалидацию элемента в любых случаях — невозможно
странно, мне казалось все просто. какой объект разрушается, такие указатели и не валидны.
я понимаю, что семантически после удаления элемента в векторе, все последующие элементы перемещаются вправо, но по факту элементы валидны, кроме последнего. вы также ничем не гарантируете, что у объекта на стеке не вызвался оператор перемещения и теперь это «немного» другой объект.
Контейнер/не контейнер — без разницы. Гарантий нет.
Это допустимо с голыми указателями, но такого не ожидаешь от смарт-указателей которые, вроде как, от этого гарантируют.
Честно говоря никогда о таком не слышал. Мне кажется это очень субъективное мнение.
Поэтому да, в моем представлении время жизни trackable-объекта должно быть всегда меньше, чем того объекта, на который он указывает. И не важно — на стеке этот объект, в контейнере или в динамической памяти.
У вас время жизни объекта ограничивается деструктором и оператором перемещения, как вы сможете реализовать свой SafePtr в таком случае?
Обязать клиента поддерживать эти инварианты?FlexFerrum
05.04.2017 10:56странно, мне казалось все просто. какой объект разрушается, такие указатели и не валидны.
я понимаю, что семантически после удаления элемента в векторе, все последующие элементы перемещаются вправо, но по факту элементы валидны, кроме последнего. вы также ничем не гарантируете, что у объекта на стеке не вызвался оператор перемещения и теперь это «немного» другой объект.
Контейнер/не контейнер — без разницы. Гарантий нет.
Эм… Нет. Это немного разные вещи. Помещение safe-указателя в скоуп времени жизни указываемого объекта как раз таки гарантирует, что никто (ну, ок, почти никто :) ) не влезет и ничего не поменяет. То есть вы не можете с SafePtr на стековый объект снять копию и использовать её после выхода за скоуп указываемого объекта — у вас стрельнёт либо ассерт на локе, либо вылетит исключение. Есть кейсы с многопоточностью, когда какой-то поток может «сбоку» что-то там подправить в контейнере, но это уже типичный data race и надо лечить именно его.
Аналогично с контейнерами — ситуация, когда вы снимаете с элемента контейнера указатель или итератор, а потом этот элемент инвалидируете — в целом, ошибочная. То есть ваш код должен быть написан так, чтобы исключить такие вещи. А не помогать их «мягко» обойти. :) А приведённый в комментарии выше пример с кешем — там в map'е лучше сразу trackable-указатели хранить (или как-то так), чтобы дать клиенту гарантию, что удаление элемента из контейнера инвалидирует всё, что связанно именно с этим элементом.
У вас время жизни объекта ограничивается деструктором и оператором перемещения, как вы сможете реализовать свой SafePtr в таком случае?
Обязать клиента поддерживать эти инварианты?
Не совсем понял вопрос.lexxmark
05.04.2017 11:08ситуация, когда вы снимаете с элемента контейнера указатель или итератор, а потом этот элемент инвалидируете — в целом, ошибочная. То есть ваш код должен быть написан так, чтобы исключить такие вещи.
Не совсем понял вопрос.
То есть технически надо сделать так, что бы мы «стреляли» не только при удалении объекта, но и при перемещении/копировании в него нового объекта.
Вопрос как это реализовать?FlexFerrum
05.04.2017 11:39Это тонкий семантический вопрос. :) В случае стековых объектов/объектов в динамической памяти всё, вроде, ясно — вы объявляете переменную (или создаёте объект по new), и у вас появляется некая сущность, над которой можно выполнять операции. Чаще всего — полностью контролируемые вами. Если вы хотите что-то сделать с таким объектом — вы (чаще всего) сделаете это явно — присвоите ему новое значение, скопируете, выполните метод и т. п. Таким образом, safe (или trackable) указатели на такие объекты имеют строго определённую семантику: они контролируют доступ к явно объявленному объекту, что бы потом с ним не происходило. А не к занимаемой объектом памяти (которая идёт как-бы прицепом).
А с объектами в контейнерах — хитрее. В контейнерах мухи — отдельно, котлеты — отдельно. Память распределяется аллокатором и потом контейнер обращается с ней так, как ему заблагорассудится. «Захотел» — разместил в ней один объект, «захотел» — переразместил другой. «Захотел» — вообще отволок объект в другое место. Таким образом выходит, что указатели, которые в первом случае контролировали доступ к объекту, который занимает какую-то память, в случае с контейнером начинают контролировать доступ к памяти, которую занимает какой-то объект. То есть несколько, гм, меняют свою семантику. Поэтому в случае контейнеров я бы вёл речь о стабильных итераторах, которые либо «указывают» на объект (элемент контейнера), на который были получены, либо явным образом инвалидируются и их невалидность можно проверить. С моей точки зрения, это более точно соответствует существующим контрактам контейнеров.lexxmark
05.04.2017 20:43Возможно это просто полиси в trackable, которое указывать как интерпретировать перемещение/копирование объекта: либо должны инвалидироваться ссылки, либо нет.
Если не возражаете, добавлю такой вид указателей как ваш SafePtr к себе в проект, ну и заглушку с простым указателем с таким же интерфейсом для консистенции? Что бы можно было легко переключать какие указатели использовать в коде.FlexFerrum
05.04.2017 23:23Возможно и policy. Но я бы предпочёл какое-то однозначное поведение.
А этот SafePtr — да, берите. Я себе ещё сделаю. :)
FlexFerrum
05.04.2017 01:12В общем, мой вариант SafePtr'а «в лоб» (на базе std::shared_ptr/std::weak_ptr) — здесь. Аккуратно стреляет при обращении к протухшему указателю. Без каких либо оптимизаций и специализаций под std::shared_ptr/std::unique_ptr. Чисто proof of concept.
И да, не пугайтесь — это C++17. :)lexxmark
05.04.2017 09:09любопытно…
я смотрю m_refs а с ним и AddRef/Release вообще не нужны там: о)
вы делаете неявное предположение, что SafePtr<T, detail::OwnerTag> должен жить столько же, сколько и объект, на который указывает.
По сути можно выкинуть TrackableBase и Trackable.
Переименовать SafePtr<T, detail::OwnerTag> в Trackable, и оставить там shared_ptr<Trackable, empty_deleter>, который проинициализировать this.
A SafePtr<T, detail::ReferenceTag> переименовать просто в SafePtr[T] и иметь указатель на T и weak_ptr[Trackable].
Идея ясна. Более эффективно убрать shared_ptr/weak_ptr и продублировать их функциональность с помощью std::atomic_int, что вы, наверное, и хотели сделать. Таким образом мы убираем аллоцирования в shared_ptr.
Как я уже говорил, у меня была схожая идея, но она не работает с аллокаторами. Когда один trackable следит за временем жизни нескольких объектов.
И да, не пугайтесь — это C++17. :)
SafePtr(T* obj) : m_trackable(new detail::Trackable(obj))
Шо, make_shared до сих пор не завезли???: о)
Кстати, safe_ptr от electronics arts мне совсем не понравился.FlexFerrum
05.04.2017 09:38Отвечаю без цитат, ибо с мобилы.
Да, то, что такой SafePtr живёт в скоупе жизни указываемого объекта — это основное предположение. Собственно, именно за счёт него и можно предоставить все необходимые гарантии. Но из-за того, что указатели бывают shared — придётся в этом коде сделать ряд приседаний, чтобы именно в этом случае SafePtr мог спокойно пережить выход за скоуп owner'а.
AddRef/Release воткнул для Release-версии, чтобы owner просто считал количество ссылок на него. Если при разрушении не 0 — помирать с исключением в деструкторе (привет дяде Александреску и его folly).
Схлопнуть до того количества типов, о котором вы говорите — не выйдет. Причины три: ковариантность простого указателя и указателя на константу, ковариантность указателя на базовый и на производные типы, а также специализация для shared_ptr. То есть в случае, если owner конструируется от shared_ptr, то reference-версия должна стать этаким синонимом weak_ptr для этого shared'а. В остальных случаях — указателем на потроха owner'а. Возможно, что в traceable вообще type erasure втыкать придётся.
Про работу с контейнерами — напишу отдельно. Там всё грустно, на мой взгляд. А make_shared в стандарт завезли, куда ж без него. :) Я имел в виду автоматической вывод типа при конструировании.lexxmark
05.04.2017 11:01Но из-за того, что указатели бывают shared — придётся в этом коде сделать ряд приседаний, чтобы именно в этом случае SafePtr мог спокойно пережить выход за скоуп owner'а.
Здесь непонятно, что имелось в виду. Хотя для shared_ptr вообще SafePtr не нужен, просто для полноты?
AddRef/Release воткнул для Release-версии, чтобы owner просто считал количество ссылок на него. Если при разрушении не 0 — помирать с исключением в деструкторе (привет дяде Александреску и его folly).
Похоже вы что-то недописали в коде. или я что-то не понимаю, тут ничего не будет падать:
~SafePtr() { auto tracker = m_tracker.lock(); if (tracker) tracker->Release(); }
Схлопнуть до того количества типов, о котором вы говорите — не выйдет.
ну это я на основании вашего «proof of concept», сколько там ковариаций, столько и у меня: о)
Возможно, что в traceable вообще type erasure втыкать придётся.
как я говорил, для shared_ptr такие указатели как SafePtr вообще бесполезны — вся функциональность и так есть из коробки. По простому можно просто запретить создавать SafePtr от shared_ptr. Ну или сделать независимую специализацию trackable и SafePtr для shared_ptr и не объединять код с применением type erasureFlexFerrum
05.04.2017 11:28Здесь непонятно, что имелось в виду. Хотя для shared_ptr вообще SafePtr не нужен, просто для полноты?
Да. Именно для полноты, чтобы поддерживать разные виды указателей с соответствующей виду семантикой.
Похоже вы что-то недописали в коде. или я что-то не понимаю, тут ничего не будет падать:
Стрелять должно в деструкторе owner-версии SafePtr, которого в примере действительно нет. :D
как я говорил, для shared_ptr такие указатели как SafePtr вообще бесполезны — вся функциональность и так есть из коробки.
Тут речь о консистентности. Когда клиент указывает в параметрах метода SafePtr — он не знает, с каким именно указателем этот метод будет вызван. То есть мы получаем некую абстракцию над указателями, которая гарантирует, что объект либо будет жив в момент доступа к нему, либо (если объект внезапно умирает) в программе будет не UB на доступе к разрушенному указателю, а вполне конкретное падение.
lexxmark
05.04.2017 20:34Тут речь о консистентности.
Но при этом вы хотите разного поведения для OwnerPtr: в обычных случаях он стреляет на выходе из области видимости, а для shared_ptr гораздо дальше своей области жизни — когда будет разрушаться разделяемый объект.
Если у этих указателей семантика разная, может и называть эти указатели нужно разными типами (пусть и с одинаковым API для консистенции).FlexFerrum
05.04.2017 23:08А вот это — смотря с какой стороны смотреть. Если формулировать контракт так, что SafePtr даёт безопасный доступ к объекту до тех пор, пока объект жив. Для scoped-указателей этот доступ заканчивается вместе со скоупом. Для shared — пока жив shared. С точки зрения клиентского кода совершенно не важно, как был получен pointer. Важно, чтобы он был «жив».
FlexFerrum
04.04.2017 22:25+1А, да. owner определяется как:
templateusing owner = T;
То есть это просто синоним типа, но «видимый» на этапе статического анализа. То есть можно написать owner<unique_ptr> и спокойно пользоваться.
crea7or
Элджера читали? 20 лет изобретают сборку мусора для C++.
lexxmark
Не читал, спасибо за ссылку. Если вы его читали, вопрос, как решать проблему с висячими ссылками и почему нет ничего подобного в стандартной библиотеке?
По поводу сборки мусора, вы же конечно понимаете, что не удалять объект пока на него есть хотя бы одна ссылка и занулять указатели (или еще как-то реагировать) при удалении объекта — это две большие разницы: о)
crea7or
Потому что C++ такой язык, где всем управляет программист. Это системная разработка с некоторыми плюшками. Есть другие языки где есть сборка мусора, там, кстати, зачастую борются уже со сборщиком.
lexxmark
Мне кажется С — это язык где всем управляет программист.
А вот по поводу плюшек верно. И одной из таких плюшек явно не хватает.
crea7or
Тут пытаются threadpool протащить в стандартную библиотеку. При желании-то можно всё что угодно навернуть, вот только нужно оно? Для этого есть boost, там подобных хитрых штук много.
lexxmark
Тут соглашусь…
после стольких лет топтания на месте язык С++ переживает ренесанс и у многих зачесались ручки добавить в язык кучу «классных» штучек. Меня особо смущает proposal на 2D drawing на основе С библиотеке Cairo.
Но проблема висячих ссылок универсальная, недаром над ней сейчас активно работают и Бьярне и Герб (см. ссылки в статье). Только вот не давать run-time иструмент для решения, а пытаться частично решить с помощью статического анализа, мне кажется в корне не верна. О чем собственно и статья.
dendron
Так уже протащили же. std:async.
lexxmark
std:async решает проблему с висячими ссылками?
crea7or
Это не совсем то, в эсинке же нет контроля над потоками.
Гора Нишанова недавно слушал у Яндекса, там с тредпулом около 25 типов новых уже придумали. То есть мрак вообще. Да и эсинк это не для тех, кто думает о производительности. Скорее плюшка для тех, кто не хочет заморачиваться.