Как стать автором
Обновить

Комментарии 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 и пытается намекнуть. Впрочем, это вполне понятная профдеформация, в чем каждый варится, то он и видит. Я тоже не исключение.
НЛО прилетело и опубликовало эту надпись здесь
Ну если «легаси-причины» состоят в том, что к твоим услугам куча библиотек на все случаи жизни, которые ты можешь использовать сразу, без написания к ним всяких FFI и тем более без тяжелых NIH-синдромов типа RIIR, то такие «легаси-причины» можно только приветствовать, нет?
НЛО прилетело и опубликовало эту надпись здесь
Лол. Секрет в том, что эти проекты адаптируются в каждый конкретный момент под актуальную версию компилятора и стандарта. И нет шоковой терапии, когда мы берём код десятилетней давности и пытаемся его адаптировать.
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, я уже примерно представлял, что такое указатель и не погнушался побаловаться с ними и посмотреть, что будет, хотя, может, и не с самой первой попытки. :)
НЛО прилетело и опубликовало эту надпись здесь
Лично я делаю вот так
Ну да — 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»
Ну да, что-то я задумался, видимо)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории