Pull to refresh

Comments 94

С++ очень сложный. И очень многогранный.
Он реально крутой… Наверное. Но писать на нем не буду. Слишком велик риск остаться инвалидом.

Ну, в данном случае риск остаться инвалидом не сильнее, чем в любом языке с GC или ARC (Java, Swift) — там циклические ссылки тоже образуются на счет «раз» и вполне могут превратить объекты в точно такие же зомби (зависит от реализации GC конечно). Ничего необычного не происходит ©.
зависит от реализации GC конечно

GC справляется с циклическими ссылками. Для этого он и сделан — удалять весь мусор, на который нет активных ссылок. ARC — это не сборщик мусора. Там или нет мусора (если нет циклических ссылок), или мусор не удаляется (если они есть).

Насчёт GC — это не всегда так. Например, IIRC, в Питоне до какой-то версии GC справлялся с циклическими ссылками… за исключением того случая, когда у какого-то объекта из цепочки был кастомный _del_(), тогда он справляться переставал. Я же говорю — зависит от реализации GC.

Так в питоне вроде бы как раз не GC в классическом понимании, а подсчет ссылок.

Там и то, и другое сразу, так сказать.

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

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

Вопрос совместимости с легаси переоценен, как и преувеличена совместимость с++ с с и более старыми версиями стандарта с++
Кратко — код не соберётся, а если даже соберётся, то не факт, что будет работать так же

скажи это проектам 10+ лет на плюсах разрабатываемым. с кучей депендов на такие же плюсовые проекты

Лол. Секрет в том, что эти проекты адаптируются в каждый конкретный момент под актуальную версию компилятора и стандарта. И нет шоковой терапии, когда мы берём код десятилетней давности и пытаемся его адаптировать.
Так что — нет, я с Вами не согласен.

Сборка C++ кода даже десятилетней давности современным компилятором с указанием современного -std= (даже если там использовались вещи, которых в современных стандартах уже нет, типа auto_ptr или throw specifier) — это ГОРАЗДО более простая и безболезненная операция, чем переписывание этого же кода на каком-то другом языке, в котором нет «всякого легаси». Так что я бы не сказал, что «вопрос совместимости с легаси переоценен», это далеко не так.

А знаете, что я в этом наблюдаю? Что С++ остается в достаточно узком сегменте, где он отлично подходит. Т.е. это не истории про веб-сервисы, про сетевые монолиты, от которых отпиливают куски и переводят на более другие языки, не про распределенные сервисы, не про микроконтроллеры и встройку.
Конечно, я знаю, что про проекты вроде Scylladb (drop-in замена Cassandra) — но это частные случаи.

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

И это я ещё не упоминал проектирование вашей квартиры, выдачу вам зарплаты, лечение в больничках, дальние путешествия, дизайн всего подряд, кино, мультфильмы, научные исследования…

Но да, сфера узковата, надо расширять. Тут я согласен

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

Напишите свой браузер на Go и микросервисах без боли и страданий, кто ж вам не дает-то. Только вот что-то не пишут.

Ещё раз. Это вполне подходит под "узкий сегмент, где с++ отлично выполняет свою работу". Вы ведь правда читали, что я написал тремя постами выше?
И Вы же согласны, что не каждый Васян пишет свой браузер. Основных — полтора штука, которые есть на каждой платформе.

Он не такой уж узкий, об этом вам antoshkka и пытается намекнуть. Впрочем, это вполне понятная профдеформация, в чем каждый варится, то он и видит. Я тоже не исключение.
UFO just landed and posted this here
Ну если «легаси-причины» состоят в том, что к твоим услугам куча библиотек на все случаи жизни, которые ты можешь использовать сразу, без написания к ним всяких FFI и тем более без тяжелых NIH-синдромов типа RIIR, то такие «легаси-причины» можно только приветствовать, нет?
UFO just landed and posted this here
Лол. Секрет в том, что эти проекты адаптируются в каждый конкретный момент под актуальную версию компилятора и стандарта. И нет шоковой терапии, когда мы берём код десятилетней давности и пытаемся его адаптировать.
1. код надо писать переносимым, а не так, чтобы приходилось под каждый компилятор адаптировать
2. бывает пишешь новый код, а ему нужны старые библиотеки.
А знаете, что я в этом наблюдаю? Что С++ остается в достаточно узком сегменте, где он отлично подходит
плохо наблюдаете. с++ в той или иной мере применим везде, для чего существуют с++ компиляторы, т.е. за исключением совсем непопулярного ембеда. Его распространенность действительно легко недооценить если забывать про то, что у всяких питончиков и джав под капотом плюсы.

Это отличная история, когда плюсовики присваивают себе наработки Сишников.
Линукс, Windows (последний раз я смотрел на исходники 2000 ядро + драйвера + немного юзерспейса) — все на Сях.
Касательно питончика — Вы имели в виду либы или сам интерпретатор? Ну, так и ТО, и ТО как правило на Сях, а не на С++ и Вы сами прекрасно знаете почему это так (бинарная совместимость, манглинг и прочая-прочая).
В качестве пруфа — https://launchpad.net/~jonathonf/+archive/ubuntu/python-3.7/+sourcefiles/python3.7/3.7.4-2~18.04.york0/python3.7_3.7.4.orig.tar.xz — вот пакет, из которого собирается альтернативный 3.7 питон к убунте. Можно точно так же открыть и оригинальный пакет. С++ там и не пахнет.
Java — положим, сама виртуальная машина. Очень даже интересно посмотреть. Только давайте договоримся о какой имплементации мы говорим, ок?

Линукс, Windows
а вы под «Линукс» имеете в виду ядро, застрявшее на си в силу фанатизма Торвальдса, или всю ось? А винду вы оцениваете какой код? Открытый?
Касательно питончика — Вы имели в виду либы или сам интерпретатор? Ну, так и ТО, и ТО как правило на Сях, а не на С++
не горячился бы я с «как правило». Взять тот же opencv — они отказались от сишного интерфейса, попросту потому что неразумно. И если вы попробуете скажем оформить питонячий модуль на плюсах и на чистом си, вы прекрасно поймете почему.
… и Вы сами прекрасно знаете почему это так (бинарная совместимость, манглинг и прочая-прочая).
внезапно плюсы умеют не только вызывать сишные функции, но и так же просто определять их.
Java — положим, сама виртуальная машина. Очень даже интересно посмотреть. Только давайте договоримся о какой имплементации мы говорим, ок?
а о какой существующей имплементации мы говорим?
а вы под «Линукс» имеете в виду ядро, застрявшее на си в силу фанатизма Торвальдса, или всю ось?

Вся ось — там такое сборище… есть ВСЕ — начиная от С, кончая перлом. Да, есть код на С++ (в первую очередь — граф. приложения). Но сказать, что он "системообразующий"… кхм… слишком смелое утверждение.


А винду вы оцениваете какой код? Открытый?

Это не имеет значения. Кто хочет — тот может посмотреть (тем более были утечки кодов NT & W2k или можно по академ подписке).


внезапно плюсы умеют не только вызывать сишные функции, но и так же просто определять их.

в обратную сторону все сильно сложнее.


а о какой существующей имплементации мы говорим?

Это к Вам вопрос. Ибо даже школьнику известно, что есть несколько разных JVM....

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

Ну и очень порадовал ответ на самый распространенный обычный вопрос «так никто не пишет! по крайней мере в библиотеках написанных профи!»
Статья очень хорошая, тут спору нет. Насчет «не пишет» — пишут, еще как пишут, в языках с ARC захватывают strong self в какой-нибудь лямбде, которую потом присваивают члену этого же класса, в Java забывают про static nested class'ы, в C++ веселятся с shared_from_this. Всякое бывает.

Не вижу тут ничего специфичного для enable_shared_from_this, обыкновенная циклическая ссылка же. Я такое могу и без enable_shared_from_this сделать.

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

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

Моя практика (да и примеры стороннего кода) показывает, что обычно циклическая ссылка возникает всё же при помощи std::enable_shared_from_this.
И я бы даже сильнее сказал: если Вам захотелось задействовать std::enable_shared_from_this — вероятно, Вам надо чинить архитектуру.
Всякая селёдка — рыба, но не всякая рыба — селёдка.
Не каждое место в C++-коде, требующее повышенного внимания, вызвано применением std::enable_shared_from_this. Но каждое место применения std::enable_shared_from_this требует повышенного внимания.

Я бы сформулировал по-другому. Всякая операция захвата shared_ptr в лямбде требует повышенного внимания.

Лямбда — это всего лишь краткий способ попросить компилятор сгенерировать для меня класс с определёнными полями данных, определённым конструктором, определённой функцией operator(), а также создать его экземпляр.
Точно такого же эффекта я добьюсь, если напишу подобный класс руками. Только это более многословно.

А в примерах SimpleCyclic и PimplCyclic operator() всё равно не используется, т.е. достаточно только конструктора и поля данных.

Как собираетесь такие места выделять в коде для обращения на них повышенного внимания?

К сожалению, для valgrind или ASAN тут практически нет шансов. С их точки зрения всё отлично до последнего момента, т. к. память не утекла и находится у другого потока. И по завершению detached thread зачищается системой. При этом в нём самом тоже толком ничего не утекло.

Какое отношение вызов detach имеет к зачистке системой? Поток продолжает жить и после detch(), только деструктор std::thread перестаёт быть блокирующим.
Кроме того, аналогичного поведения можно добиться вообще без вызова detach и даже без запуска потока в пользовательском коде. Примеры из документации буста легко переделать под такое — надо только не блокировать главный поток вызовом run(), а запустить в нём polling-loop с вызовами boost::asio::io_context::poll().

Суть в том, что ситуация завершения по exit() при нескольких живых потоках для ASAN нормальная, а для valgrind – почти нормальная. И на момент завершения утечки нет, т.к. ресурсами владеет как раз "утекший" поток.

сначала не понял почему из всех языков и способов создать циклическую ссылку автору столь нелюбим именно std::enable_shared_from_this. Однако потом до меня дошло… что дело не в нём, точнее, не совсем в нём. Грубо говоря, если из области применимости shared_from_this исключить часть, где достаточно простого shared_ptr, останутся сценарии «классу нужно оперировать умными указателями на собственный инстанс». Класс, управляющий собственным лайфтаймом — известный антипаттерном. И называется он не «зомби».

Впрочем, практически любое использование shared_from_this можно переписать через pimpl.
Например так
class MyClass {
    ... // copyable, movable, etc. handle to data
private:
    class MyClassPrivate;
    std::shared_ptr<MyClassPrivate>;
};

Pimpl на базе std::shared_ptr в статье приведён.
Если Вы имеете в виду, что при таком построении пимпла фасад может подсунуть реализации ссылку на неё же саму — то да, может. И получится то же самое, что с std::enable_shared_from_this. Это ситуация типа «захотелось применить std::enable_shared_from_this, но не знали о таком, поэтому сделали то же самое, но без перламутровых пуговиц». И проверить будет сложнее, т.к. поиск по shared_from_this не покажет такого места.

Антипаттерн «Зомби» — не просто про управление классом собственным временем жизни. Это ещё цветочки, которые могут работать корректно.
Пример SteppingZomby в статье отработал так:
Вывод в консоль
N13SteppingZomby5ZombyE::resolveDnsName started
N13SteppingZomby5ZombyE::resolveDnsName finished
N13SteppingZomby5ZombyE::connectTcp started
============================================================
| Zomby was killed |
============================================================
N13SteppingZomby5ZombyE::connectTcp finished
N13SteppingZomby5ZombyE::establishSsl started
N13SteppingZomby5ZombyE::establishSsl finished
N13SteppingZomby5ZombyE::sendHttpRequest started
N13SteppingZomby5ZombyE::sendHttpRequest finished
N13SteppingZomby5ZombyE::readHttpReply started
N13SteppingZomby5ZombyE::readHttpReply finished
N13SteppingZomby5ZombyE::~Zomby
N6Common22WriteToConsoleListenerE::~WriteToConsoleListener


А мог бы отработать вот так:
Вывод в консоль
N13SteppingZomby5ZombyE::resolveDnsName started
N13SteppingZomby5ZombyE::resolveDnsName finished
N13SteppingZomby5ZombyE::connectTcp started
============================================================
| Zomby was killed |
============================================================
N13SteppingZomby5ZombyE::connectTcp finished
N13SteppingZomby5ZombyE::~Zomby
N6Common22WriteToConsoleListenerE::~WriteToConsoleListener


Управление собственным временем жизни могло быть в обоих случаях. Но иногда оно создаёт весьма сложные для детектирования и отладки проблемы, а иногда — нет.
Т.е. «Зомби» — это когда уже «доуправлялись собственным временем жизни». Когда абстрактная кривизна архитектуры превратилась в большущие, но при этом хорошо замаскированные проблемы. Вроде код работает. И автотесты проходят. И динимаческий анализ молчит. И статический тоже молчит. Ну может в логах оно отбивается (но если отбивается — значит логи многокилометровые, и следы зомби там могут теряться годами).
Ведь в примерах SimpleCyclic и PimplCyclic тоже есть управление собственным временем жизни. Но это не то, оно не так опасно — и динамическим анализом находится, и на практике я не знаю ситуаций, в которых захотелось бы так написать.
А для активных сущностей — иногда именно так и пишут.
Если Вы имеете в виду, что при таком построении пимпла фасад может подсунуть реализации ссылку на неё же саму — то да, может. И получится то же самое, что с std::enable_shared_from_this. Это ситуация типа «захотелось применить std::enable_shared_from_this, но не знали о таком, поэтому сделали то же самое, но без перламутровых пуговиц»
нет, я про то, что можно переписать код так, чтобы время жизни класса и его логика были разнесены по разным сущностям. А обратить внимание очевидно надо будет на те места, где в инстанс класса (в моем примере MyClassPrivate) передается shared_ptr на него же. И это намного проще, потому что привлечет внимание еще на этапе написания кода, а не во время отладки
И проверить будет сложнее, т.к. поиск по shared_from_this не покажет такого места
а вы какую задачу хотите решить — написания корректного кода, поиска багов или поиска мест где баги высоковероятны?
О чём статья — написано в заголовке: я предупредил в ней об опасности.

Надо ли разбирать методы устранения опасности — посмотрим по опросу через пару дней.

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

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

Предлагаю не обсуждать методы исправления — и так материал довольно объёмный, уже 60 комментов настрочили, а эта тема потянет ещё на целую статью такого же объёма.
А не подскажет ли мне кто-нибудь вот какую штуку.
Вот есть у класса функция типа
void CNet::SetInput(std::shared_ptr<CInput> cInput_Ptr)
{
 cInput_Local_Ptr= cInput_Ptr
}

То есть, я просто указываю классу указатель на другой класс, который он запоминает для дальнейшего использования как входной параметр.
А вот дальше я не понимаю, как передать указатель, например, на такое (независимот от того, как был создан cData):
class CData
{
  public:
   CInput cInput;
};
CData cData;

cNet.SetInput(&cData.cInput); // вот как это сделать? Объект cInput не создавался динамически напрямую.


Простой путь — это завести в классе CData поле именно типа std::shared_ptr (я надеюсь, что между классами CInputClass и CInput есть связь типа «наследование»?)
Если выставляете поля данных в public — это уже скорее struct, чем class.
я надеюсь, что между классами CInputClass и CInput есть связь типа «наследование»?


Опечатался. :) Это один и тот же класс. Исправил.

Простой путь — это завести в классе CData поле именно типа std::shared_ptr


Ну это-то очевидно. Но вот как без этого?

Если выставляете поля данных в public — это уже скорее struct, чем class.


Это для примера. Реально у меня классы — слои свёрточной нейросети и я хочу соединять входы и выходы (вход — указатель извне, выход — собственность класса слоя) и создавать классы обучения слоёв, порождаемые самими слоями (с передачей this в порождаемый класс обучения). И вот захотелось мне просто передать статический объект по указателю. А так как всё через shared_ptr сделано, то и возник вопрос, а как это сделать? Пока решение вижу одно — выкинуть все эти shared_ptr в параметрах функций типа SetInput нафиг и поставить и хранить обычный указатель. Тогда можно будет абсолютно любой указатель передавать, хоть умный, хоть нет и на что угодно указывающий. Только это, как я понимаю, сейчас совсем не приветствуется.
Если хотите сложностей — смотрите в сторону std::shared_ptr с deleter-пустышкой. Но судя по вопросам, до корректного применения подобных техник Вам ещё года 2-3 покоддить бы… Ну и — я честно не понимаю, зачем плыть против течения.

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


Ну это рекомендация в стиле «тут так принято». Типа карго культа. А мне вот интересно, как именно в такой ситуации быть, если функция хочет shared_ptr, а объект статический. Перегружать deleter это как минимум некрасиво, так как извне неочевидны причины такой перегрузки.

Сырые указатели легко превращаются в сырые указатели на уничтоженные объекты.


Зависит от подхода и стиля. Если привык к «щас сбацаем и пусть умный указатель следит сам», то да, при переходе к обычным указателям начнутся утечки памяти из-за отсутствия опыта отслеживания всего созданного. А если привык сам следить за всем и не создавать динамические объекты без надобности, тогда нет. За 19 лет ни разу сырые указатели у меня не превратились в указатели на уничтоженные объекты. В конце-концов, не часто программе требуется постоянно что-то динамически создавать — обычно, при запуске создаются нужные объекты, а при завершении они уничтожаются.
Если бы автору вопроса были «даны свыше» (из какой-нибудь библиотеки) и класс CData, и класс CNet в том виде, в котором они присутствуют в вопросе — то да, это была бы интересная задачка. Решение с пустым deleter — первое, что пришло в голову, и наверняка есть ещё 2-3 варианта получше.
Но, судя по всему, тут ещё не до полицейских разворотов — на второй передаче научиться бы ездить.
Решение с пустым deleter — первое, что пришло в голову, и наверняка есть ещё 2-3 варианта получше.


А может, вариантов получше и нет.

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


Я вот не пойму, какой вам резон уже во втором сообщении пытаться меня уязвить по типу «мал ещё»? Да, я не часто использую эти умные указатели (у меня на основной рабочей системе QNX 6.3 просто вообще нет Си++ 11 компилятора. Вот нет и всё.) и гораздо меньше озабочен трюками с ними — меня волнует решение задачи пользователя и читаемость программы, а не нюансы извращений с умными указателями, пересыпанные костылями вроде enable_shared_from_this. И поэтому вполне логичен вышеобозначенный вопрос. Он элементарен для обычного указателя (да он даже не возникает в этом случае), но вызывает проблемы с умным.
Эммм… Простите. Если честно — не посмотрел на авторов комментов. Первый Ваш коммент просто слишком сильно диссонирует с последующими, чтобы можно было предположить, что они принадлежат одному человеку.

Если по существу вопрса — лучше в личку.
Если по существу вопрса — лучше в личку.


Так а зачем? Я думаю, варианты решения много кому будут интересны.
Может кто знает ещё красивые методы решения.
Надо как-то согласовать сырой указатель с умным.
Сырой указатель берётся из операции взятия адреса у какой-то штуковины, о времени жизни которой кто-то уже заботится.
Умный указатель по умолчанию тоже заботится о времени жизни той штуковины, которой его проинициализировали.
Итого: поведение по умолчанию приведёт к двойному удалению, что является UB.
Вы хотите это поведение изменить.
Для этого надо, чтобы кто-то один перестал заботиться о времени жизни этой штуковины.

В случае с std::shared_ptr этого можно добиться подсовыванием пустого deleter. Аналога std::unique_ptr::release() в std::shared_ptr нет и быть не может, а если бы и была — она должна была бы вызываться постфактум, т.е. примерно в деструкторе CNet. А вот пустой deleter можно подсунуть в том месте, где будете наводить связь.
Ну а как перестать заботиться о времени жизни штуковины, не живущей в умном указателе? Вероятно, просто воспользоваться new без последующего delete, т.к. со штуковиной, созданной на стеке, так не получится.

Первый вариант приводит к странной спорной конструкции, нуждающейся в пояснениях комментарием.
Второй вариант естественным образом приводит примерно к:
auto input = std::shared_ptr<CInput>(new CInput);

, и далее — прямо к улучшенной версии:
auto input = std::make_shared<CInput>();


Первый вариант — редкостное извращение, второй вариант — обычное штатное использование std::shared_ptr.

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

Итого: поведение по умолчанию приведёт к двойному удалению, что является UB.


Именно.

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


Разве я не могу в тестовой функции создать все нужные классы на стеке, создать к ним shared_ptr без deleter'а и их уже передать в методы нужных классов? Методы отработают, функция завершится. Объекты разрушатся.

А если Вы не контролируете обе стороны этой связи — то прошу объяснить, как так вышло.


Тут дело просто в том, что не хочется всё создавать динамически. Поэтому я и спросил, какие есть вообще решения.
Разве я не могу в тестовой функции создать все нужные классы на стеке, создать к ним shared_ptr без deleter'а и их уже передать в методы нужных классов?

Можете. Но зачем бороться с RAII вместо того, чтобы его использовать? Ну я не знаю — это как вытащить из микроскопа оптику, чтобы он не сломался при забивании гвоздей.

Умные указатели совместимы с сырыми только в одну сторону. Если Вам одна и та же штуковина в одном месте нужна в умном указателе, а в другом — в сыром, то проще создать в умном и отдать туда и туда.

не хочется всё создавать динамически

Почему?

Если Вы так не любите/не умеете пользоваться умными указателями — зачем тогда:
void CNet::SetInput(std::shared_ptr<CInput> cInput_Ptr)

?
Почему?


Просто не нравится. :)

Если Вы так не любите/не умеете пользоваться умными указателями — зачем тогда:


Нет, я умными люблю пользоваться (конечно, они ведь удобны), но иногда хочется смешать, как в указанном примере. А функция принимает умный потому, что хочется в то же время в современном стиле Си++ всё-таки писать. А то опыта работы с ними и понимания их ограничений не будет. Без проблемы ведь не появится понимание метода решения. Да и вообще, бывает, придумаешь решение, а компилятор с ним работает не так, как задумывалось. Вот например. gcc 2.95 это компилирует, но работает неправильно — указатель приводится к базовому классу с вызовом функции без реализации (изначально — в примере там заглушка с выводом «CErrorsBased!»). Современный компилятор такое уже не компилирует вообще. А идея использования задумывалась интересной, но всё обломала реализация языка. :)
С чисто технической точки зрения вполне можно сделать, чтобы этот код работал:

godbolt.org/z/uzTQzc

Виртуальное наследование в конечном классе совершенно излишне, ну и все ж вызов set_function_ptr() я бы через std::invoke переделал.
Виртуальное наследование в конечном классе совершенно излишне,


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

я бы через std::invoke переделал.


Не могу. Это 17-й стандарт, а это фрагмент сделан по мотивам даже не 11-го (это было в моей QNX программе).
Достаточно, чтобы первый уровень наследования от общего класса был виртуальным. А почему вообще возникает эта ошибка — потому, что приведение к виртуальному базовому классу в compile time в общем случае невозможно из-за деталей реализации этого механизма.
Достаточно, чтобы первый уровень наследования от общего класса был виртуальным.


Да, в данном случае каждый из объектов получит ссылку на этот базовый класс вместо его самого.

А почему вообще возникает эта ошибка — потому, что приведение к виртуальному базовому классу в compile time в общем случае невозможно из-за деталей реализации этого механизма.


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

Ну, как грицца, не каждая идея выдерживает свою реализацию, хехе.

но всё обломала реализация языка

Это признак того, что Вы сражаетесь со своими инструментами.

Просто не нравится

«Просто не нравиться» может цвет обоев. Или вкус еды. Или музыка. Или текстура ткани.
А в технических решениях принято взвешивать «за» и «против», и выбирать в соответствии с этим. Если решения при взвешивании оказываются эквивалентными — ну что ж, тогда дальше выбор за автором. Иногда конечно говорят «мне это решение не нравится», но обычно это означает что-то типа «чутьё подсказывает, что это приведёт к плохому коду, но вербализовать недостатки вот прямо на ходу не могу», или «я вижу, что недостатки перевешивают и могу это вербализовать, но это долго, а Вы вроде как и сами должны их видеть». Это совсем не то, что «настоящее» «не нравится».

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

Касательно кода из примера — в личку. Слишком далеко ушли от темы статьи.
Это признак того, что Вы сражаетесь со своими инструментами.


Нет, это признак того, что не всё придуманное реализуемо средствами формальной системы языка. :)

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


Вот вы сами и ответили на этот вопрос. :)

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


Вообще-то, делал и это. Знаете, как Windows98 крашилась если под MS-DOS в Watcom выделить память через new больше, чем есть в системе и сделать её очистку? Это было интересно. :)
Вот как раз тот код, который у Вас уже возник, и является плохим. Не надо так делать. А чтобы это понять — надо просто взять и попользоваться умными указателями в тех сценариях, для которых они предназначены. Хотя никто не заставляет — плюсы, к счастью, позволяют выбрать комфортное для себя подмножество языка и не выходить за его пределы (конечно, если этого не требует работодатель или ещё какие сильные обстоятельства).

Вообще-то, делал и это

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


Ну, код проекта тут я ещё не приводил — я привёл упрощённый тут же написанный пример. Сам же проект перестраивался раза 4, ибо придумать удобную архитектуру построения CNN и не запутаться в ней сходу не удалось.

И что, прям вот первой попыткой использования сырых указателей решили «а дай-ка я посмотрю, что будет»?


Я начинал с ZX-Spectrum с его ассемблером и бейсиком, а потому к моменту появления у меня IBM, я уже примерно представлял, что такое указатель и не погнушался побаловаться с ними и посмотреть, что будет, хотя, может, и не с самой первой попытки. :)
UFO just landed and posted this here
Ну да — std::shared_ptr с пустым deleter. Который предоставляет пользователю, не знакомому с подробностями реализации, нечто с весьма неожиданными свойствами.
Зачем сначала брать умный указатель, а потом кастрировать его до состояния глупого, но только с повышенными накладными расходами? Если Вам не нужно поведение умного указателя — зачем Вы его используете?
Потому что остальной код принимает в качестве параметра shared_ptr, чтобы было можно передавать туда.
От std::shared_ptr там только название (более длинное, кстати), плюс повышенные накладные расходы по процессору и памяти.
А поведение — тадам — от сырого указателя.
Исходя из того, что внешние сущности успешно работают с возвращаемым кастратом std::shared_ptr — их устраивает именно поведение сырого указателя.
Вот его и надо возвращать, а не пытаться сделать «современно» — получается какое-то уродство.
Циклы из shared_ptr это банальщина и enable_shared_from_this просто еще один из миллиона способов их сделать. А вот weak_ptr и make_shared это посерьезнее проблема. Когда все пользовательские деструкторы выполнились, а память в систему не вернулась.
Кратко перескажу содержание предыдущих серий (видимо, читать что статью, что комментарии Вам лень): если у Вас возникло желание использовать std::enable_shared_from_this — Вы в шаге от создания циклической ссылки. И при некоторых обстоятельствах это приведёт к трудонодетектируемой проблеме.
> если у Вас возникло желание использовать std::enable_shared_from_this — Вы в шаге от создания циклической ссылки. И при некоторых обстоятельствах это приведёт к трудонодетектируемой проблеме.

Тут не то, чтобы желание — например, весь boost::asio на этом построен. И ничего. При аккуратном использовании в силу понимания происходящего он вполне себе торт.
При аккуратном использовании в силу понимания происходящего

Вот это вот ключевое.
При аккуратном использовании всё что угодно вполне себе торт — хоть топор, хоть змеиный яд, хоть атомная бомба. Но такая оговорка не делает приведённые вещи безопасными.
На мой взгляд, в первом примере, после легкомысленной инициализации capture group результатом shared_from_this() вполне логичной выглядит необходимость оставаться в живых, пока тебя могут дернуть за _fn.
Так что я даже не знаю, std::enable_shared_from_this ли виноват.
С таким же успехом можно было бы попытаться захватить невинно выглядящую ссылку, и почувствовать себя в среде immutable by default.
Простите, а кто может «дёрнуть за _fn»? Он разве выходит наружу?
В коде нет никаких предпосылок думать, что _fn предназначен для какого-либо использования снаружи, тем более — после уничтожения родительского экземпляра SimpleCyclic (Вы же SimpleCyclic имеете в виду, говоря «в первом примере»?)
Более того, _fn в примере вообще никем и нигде не вызывается, даже внутри класса.
И написана эта конструкция именно так только по одной причине — это просто экстремально лаконичный способ организации циклической ссылки. И всё. Без высоких целей и идеологической подоплёки.
>Можно остановить зомби, уничтожив экземпляр boost::asio::io_context!

Или просто закрыть стрим, тогда async_read вызовет коллбэк с ошибкой и все остановится корректно.
А ничего, что в примерах из буста stream, в отличие от io_context, является полем данных класса зомби?
Ну так надо метод сделать для его закрытия.
Антипаттерн здесь вовсе не в shared_from_this, а использовании асинхронного чтения в runOnce. Тут надо или читать синхронно или дожидаться завершения и уж потом выходить.
shared_from_this во всех примерах статьи является отличным инструментом для завязывания в узел.
Антипаттерн здесь вовсе не в shared_from_this, а использовании асинхронного чтения в runOnce

Эммм… Это как? Т.е. если мне поставлена задача написать асинхронную реализацию соединения — я должен ответить, что это — антипаттерн, и настаивать на том, чтобы отказаться от асинхронности в пользу блокирующих вызовов?
дожидаться завершения и уж потом выходить

Ага. Т.е. запущенный процесс ну никак нельзя прервать на уровне бизнес-логики. Просто прелесть.
Так метод тогда должен называться не runOnce а типа invoke...., и соответствующими методами класса для контроля над асинхронными процессами. У вас же претензия в том, что чтение продолжает работать после завершения runOnce.
runOnce запустил какой-то анинхронный процесс. Чем метод запуска асинхронного процесса должен отличаться от метода запуска блокирующего процесса? Комментарием? Да, комментария не хватает, каюсь.
Например возвращать не void, а некоторую сущность, дающую доступ к этому асинхронному процессу. Главное, чтобы была возможность контроля над этим асинхронным процессом. В противном случае любой асинхронный процесс рискует стать зомби.
Можно и так, да.
Но почему Вы утверждаете, что этот способ — единственно верный?
Конструкция с Listener тоже не только способна выполнять роль асинхронного интерфейса, но и — о ужас! — иногда даже применяется в production в таком качестве.

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

Так ещё Herb Sutter в прошлогоднем (вроде) докладе говорил: «Не использовать shared_ptr для кольцевой, только weak_ptr. Если захватить нечего, то и результат никому не нужен.» Даже с многопоточность такое работает в некоторых случаях.


https://youtu.be/xnqTKD8uD64

Почитайте внимательно мой пример BoozdedZomby.
И попробуйте его исправить, переделав на слабую ссылку.
А там и поговорим.
Ок, бывает нужным чтоб экземпляр держал последнюю (резервную) ссылку на самого себя до момента завершения управляемого им какого-либо потока / коллбэка (чтобы клиентский код не прибил его из-за удаления экземпляра; или же просто нужно гарантировать жизнь объекту-контексту до конца выполнения потока, который просто не возможно взять и в любой момент завершить). Вот это есть антипаттерн «зомби»? Какие предложения?
Нет, не это. Это только приближение вплотную к антипаттерну «Зомби».
Антипаттерн — это когда додержались до того, что оно продолжает что-то делать из-под капота, когда пора бы уже остановиться.
Му-ха-ха… И я ж как раз использовал те примеры из boost::asio для своих задач не особо вникая в суть происходящего, считая, что выглядывать баги в оф.примерах — вот уж это настоящая паранойя… Хе-хе, оказывается-то вовсе нет… Фак.
Спасибо за отличный разбор проблемы!
Наверное, тут должно было быть ==?
'if (auto read = s.read())'
Нет, всё корректно.
if statement
«declaration of a single non-array variable with a brace-or-equals initializer»
Ну да, что-то я задумался, видимо)
Sign up to leave a comment.

Articles