Comments 21
Bacteria() = default;
Bacteria(const Bacteria&) = default;
Bacteria(Bacteria&&) = default;
Bacteria& operator = (const Bacteria&) = default;
Bacteria& operator = (Bacteria&&) = default;
~Bacteria() = default;
мой бог, Чудовищное(tm) зрелище!!! вот за это ненавидят и боятся C++.
уже давно писал и объяснял: переходите на sh_ptr/mem_pool и получите МНОГОКРАТНОЕ ускорение без мути https://ders.by/cpp/norefs/norefs.html#4.3
ЗЫ плюс научитесь различать Чудовищное...
Не вижу ничего ЧУДОВИЩНОГО в том, чтобы явно указать конструкторы и операторы с помощью ключевого слова default. Можно обойтись и без них. Но на мой взгляд "явное лучше неявного" (с). Я даже почитал книжку, на которую вы ссылаетесь. В книге довольно много сомнительных тезисов и много пафоса. Нет, не пафоса, а Пафоса.
А в чем смысл деструкторов, если памятью управляет другой объект? Перешли на арены, ну и молодцы. Модно, молодежно, безопасно. И тут мы поверх этой радости зачем-то добавляем RAII.
Когда я хочу себе сделать больно, я просто достаю бритву.
Если внимательно прочесть статью, то вы сможете увидеть, что проблема как раз таки в лайфтайме арены. И нет, это не безопасно.
В парадигме арен у нас есть три основных категории лайфтаймов - статический, который почти никогда нет нужды очищать, сессионный под каждый http-запрос\кадр для рендера\что-там-у-вас и черновик для одноразовых штук, которые в языках с GC помирают в яслях. Зачистка памяти нужна в конечном числе самоочевидных мест. Проблемы могут возникнуть если ты перепутал лайфтаймы, что фиксится тривиальной обёрткой типа arena_ptr<int HEAP, T> или, что и происходит в статье, если ты внутри одной системы контроля за ресурсами добавляешь еще одну, ортогональную первой потому что, я не знаю, не на что списать часы в Джире.
В остальном это не сильно опаснее джавы.
В статье буквально рассматривается пример, где статический лайфтайм ставит вам палки в колеса. Более того, вы храните эту память до конца программы, даже если она вам не нужна. Так что нет, статический лайфтайм не подходит. Сессионный - тоже. Из-за тормозов при каждом обращении к системе. Если выделять арену каждый раз, когда работаете с большим количеством объектов, то так можно. Но это буквально ведёт вас прямым путём к менеджеру памяти. Просто потому что после завершения "сессии" вам нужно будет чистить эту арену.
С полиморфическими аллокаторами столько разных тонкостей, например - деградация move-assignment, AA, а вы выбрали написать про костыли для лайфтаймов, ортогональные с самими аллокаторами
Для понимания большинства ньюансов достаточно изучить P2127, P2035, P2126, при желании игнорируя легаси и специфику BDE
Спасибо за комментарий по делу. В статье под "отличающимся от привычного" поведением в том числе и имелась ввиду деградация move-семантики. В основном статья и напирает на различия полиморфных аллокаторов и стандартных. И на то, что нельзя просто взять и поменять одно на другое.
Посыл полезный, но в данном случае почти все проблемы решались бы передачей аллокатора извне по цепочке, попутно делая ваши типы allocator-aware при необходимости. Это и есть центральная идея pmr, и было бы полезно увидеть демонстрацию "как надо"
Корректность такого кода довольно легко было бы проверить 1) статически, проверяя на соответствие требованиям AA (в стандарте готового шаблона нет, но пишется он один раз для всех типов) 2) в юнит тесте, делая инъекцию аллокатора с подсчётом аллокаций, и выставляя аллокатором по-умолчанию другой аллокатор, бросающий исключение при аллокации
Далеко не всегда есть возможность передавать аллокатор по цепочке. Особенно это не удобно, если вы используете third-party. Но да, это выход, пусть и не удобный.
А можно поподробнее, в чем тут конкретно неудобность? На примере third-party
Ну, чтобы добавить передачу аллокатора через параметр, вам необходимо поменять интерфейс. Если часть цепочки - third-party без возможности редактирования кода, то у вас ничего не выйдет. Кроме того, опять же, вам нужно менять интерфейс. Что если ваш текущий интерфейс без параметра-аллокатора уже просочился в 10001 место в коде? Опыт подсказывает, что очень часто поменять интерфейс даже у маленького класса == много боли. Подход с менеджером ресурсов же позволяет вам избежать этого.
std::pmr::memory_resource* resource = std::pmr::get_default_resource() последним параметром?
Зачем вам вообще иначе pmr? Используйте обычный std::allocator
То что у вас должно было быть в примере - AA тип со всеми вытекающими, включая прием аллокатора в clone, и вы просто передаёте свой аллокатор в std::pmr::deque, который уже сам все передаст дальше
Никаких placement-new/вызовов деструктор в, у вас для этого методы polymorphic_allocator есть (не путать с memory_resource). И никаких менеджеров-синглтонов. В крайнем случая адаптер, который будет управлять аллокатором
Кажется, вы игнорируете принципиальное отличие pmr от не-pmr. Оно вовсе не в наличии нескольких имплементациий аллокаторов в стандартной библиотеке. Оно про скалируемость разработки ценой рантайм полиморфизма, которая с обычными аллокаторами просто ужасна
Но герой вашей статьи вместо упрощения усложняет свою жизнь 🙂
А я теперь хочу статью про другие тонкости, например деградацию move-семантики.
Немного не понял приседания с оператором копирования:
если вы допускаете в контейнере элементы с разными пулами, то при копировании не нужно копировать m_pool. Он (m_pool) ведь должен указывать на пул своего m_genes (это такой архитектурный костыль), соответственно, если не меняется пул в m_genes, то и копировать его в m_pool не нужно. Да, в разных элементах будут несколько пулов. Как говорится, бай дизайн;
если при копировании нужно взять пул от источника, то (по-идее) должно сработать копи (с отработкой исключений) и потом своп. Вы это сделали через:
this->~Bacteria();
new (this) Bacteria(std::move(copy));
но почему обычный своп (напишем свою функцию) не подойдёт? По-идее и исключений быть не должно.
Возможно, я что-то не понял, может распишете чуть подробнее.
Тема всё равно интересная.
Да, постараюсь чуть подробнее расписать. Если вы про то, что можно использовать тот пул, который изначально используется в *this
, то да, так можно. Тогда вам нужно будет всего лишь не копировать m_pool
. Это даже сработает и в случае перемещения. Но есть нюанс. Если вы посмотрите в документацию по любому оператору перемещения в стандартных контейнерах (ну, пусть это будет std::unordered_map), то увидите кое-что интересное. Если полиморфные аллокаторы после присвоения не будут совпадать, то контейнер будет обходить каждый свой элемент и делать move-assign. Это гораздо более затратно, чем просто скопировать один указатель на выделенный буфер памяти. А еще это снимает noexept из-за потенциальных выделений памяти в новом месте. А еще если у вас контейнер - это строка, то вы еще и копирование вместо мува получаете (см. раздел Complexity). В итоге вам всё равно придётся писать костыль с вызовом деструктора и конструктора, если вы не хотите в своём классе не-noexcept move и потенциальное частое копирование вместо перемещения. А раз вам всё равно придется это делать в move-операторе, я посчитал, что и в операторе копирования тоже можно это прописать, чтобы их поведение очевидным образом совпадало, а не различалось. Но да, пример стоило делать именно на move-операторе.
Почему нельзя сделать swap? Потому что это UB (см. раздел Notes).
люди!
таки сделайте два шага от телевизора и внимательно посмотрите, что вы тут пишете...
вот эта ДЛИННЮЩАЯ ПРОСТЫНЯ текста - разве она решает хоть одну полезную задачу?
нет!
вы Героически Боритесь с врожденными недостатками STL и копирования/перемещения С++. и вы опять проиграете!
зачем тратить время на глупости, когда уже знаете, КАК программировать в удовольствие?
О том, как легкомысленное использование полиморфных аллокаторов может испортить вам жизнь