Pull to refresh

Comments 38

Даже если мы передаем параметры в функции и храним локальные ссылки как shared_ptr<T>, мы все равно рискуем, потому что this все равно передается как T*

Разве не получится избежать этого через std::enable_shared_from_this?

enable_shared_from_this позволяет конструировать shared_ptr из raw-поинтера. Но все равно при вызове любого метода нулевым невидимым параметром будет передан this в виде сырого указателя. Пример:

shared_ptr<class Card> c;

struct Card
//  : enable_shared_from_this<Card> // не важно
{ 
    int x = 100;

    void myMethod() {
        c = nullptr;
        cout << "Card acessed" << this << endl;
        x = 0;       // UB
    }
    ~Card() { cout << "Card destroyed" << this << endl; }
};

int main() {
    c = make_shared<Card>();
    c->myMethod();
}

результат будет Card destroyed... Card accessed

То что this неявно передаётся - это одно. Тут больше речь про время жизни и простоту кода. enable_shared_from_this как раз таки решает этот вопрос.

Раскомментируй строчку в моем примере: enable_shared_from_this<Card> - ничего не изменится, enable_shared_from_this не решает проблему.

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

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

У меня не стоит задачи мериться написанием кода. Я лишь указал на серьезные слабости вашей статьи. Ниже привели более интересный ответ.

Если вам качество вашей статьи безразлично, то можете просто пропустить мой комментарий :)

Я описал набор критериев, по которым будет оцениваться способность языков управляться с DOM-образными структурами данных. Среди этих критериев нет требования инкапсуляции, требования понравиться Сазонову и еще каких-то других не относяхищся к делу требований. Там есть требование не течь, не падать, быть простым и понятным. Я дал (и по-прежнему даю) всем желающим возможность написать этот тест-бенчмарк на любых языках, я подождал неделю, и не получил никакого кода, ни на одном языке, но получил тонну обвинений в том, что я хочу украсть чужой труд, что моя предлагаемая структура данных - никому не нужна, и что ее нельзя реализовать на Расте, потому что он - системный, а структура 1С-ная, что требования не падать означают, что пользоваться эти апи будут дебилы, а требование не течь невыполнимо. Я взял и за несколько часов реализовал и проанализировал этот пример на JS и C++. Что я получаю в виде фидбека? Тонну оскорблений и никакой конкретики.

У моей статьи есть цель - показать сильные и слабые стороны С++ в приложении к DOM-структурам. Мой код это показал. Если вы считаете, что что-то из отмеченных выше слабостей языка можно исправить, заменив struct->class и насовав в код public/protected - флаг в руки, и вперед, доказывать. Если нет давайте пропустим ваш комментарий.

У вас не получилось показать сильные стороны си++

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

Я про ваш код, а не про текст статьи. Что именно не так - уже написано в предыдущих комментариях.

А почему в данном примере Card::myMethod() удаляет объект, которым он не владеет?

Это фундаментальная проблема, которая лишь косвенно относится к this. Таким образом (модифицируя данные поперек владельца) можно все сломать и без this, и без указателей вообще.

А почему в данном примере Card::myMethod() удаляет объект, которым он не владеет?

Это минимальный пример, воспроизводящий проблему. В реальном примере в сфокуссированный элемент карточки пришло событие нажатой кнопки. Обработчик вызвал библиотченый метод, который вызвал обработчик события из другой легаси библиотеки, который активизировал стейт-менеджер написанный три года назад командой из Еревана, который удалил форму по идентификатору считанному из файла конфигурации. Да упало это на миллионе абонентских устройств, и баг нашелся по логам.

Вполне допускаю, что такой случай мог произойти, при наслоении разных библиотек и легаси в C/C++ что только не может произойти.

Но в статье и текущей ветке идет речь о разработке представления DOM-подобной структуры данных на C++ и поэтому возникает резонный вопрос что это за интерфейс такой у разрабатываемой структуры данных, который так легко позволяет выполнить delete this и прочие непотребства.

Вы справедливо замечаете, что надо следовать правилу вызова методов на отдельных объектах shared_ptr, только вот формулируете это как будто это такая особенность shared_ptr и с ним надо быть всегда предельно осторожным, чуть что и core dumped.

Хотя на самом деле просто любая сущность должна модифицировать только те данные, которыми она владеет. Нельзя удалять объект, если ты им не владеешь и не отвечаешь за его время жизни.

В случае shared_ptr это само собой подразумевает владение указателем. Это не выбор или быть предельно осторожным, или создавать временные shared_ptr, или что-то еще. Если код что-то делает с экземпляром shared_ptr, которым он не владеет, то нет никакого shared_ptr, вы просто работаете с самым обычным голым указателем черти на что. Конечно так дела не делаются.

В упрощенном примере основной баг в том, что main() объектом не владеет. Поэтому любая модификация глобального состояния сразу же все ломает.

И то, что this передается как T* никак к shared_ptr не относится. Указатель на this не владеет временем жизни объекта, его нельзя передавать куда-то, где указатель может быть сохранен или тем более использован для удаления. Если надо куда-то передать для управления объектом ссылку на объект изнутри метода, то это или shared_from_this(), или никак.

Вы когда-нибудь создавали продукт силами четырёх команд, разбросанных по трем континентам - с историей в десятилетия, десятками мажорных версий, пять из которых одновременно в продакшене у разных клиентов? Если да, то вы понимаете, насколько важно, чтобы структуры данных не рушились от любого чихa. Поэтому в исходной статье появилось требование "Устойчивость", а в сценариях - "Удаление карточки из метода элемента этой карточки". Если не поддержать это требование на фундаментальном уровне, компания потеряет миллионы и репутацию - и не спасут ни дисциплина владения, ни сферические архитектурные принципы в вакууме.

Вероятно, я неправильно понял тот ваш пассаж, который цитировал. Думал, что речь про передачу указателя на себя во внешние функции, а не про неявный первый параметр.

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

Согласен с соседними комментариями, что это очень неудачный пример кода.

Этот пример кода имеет единственной своей целью продемонстрировать, что enable_shared_from_this  никак не защищает объект от преждевременного удаления. И он это демонстрирует. У вас есть пример кода, который демонстрирует обратное?

Он и не должен от него защищать.

Я же написал, что упомянул shared_from_this в контексте передачи this в параметры функции. В вашем примере такой передачи нет.

enable_shared_from_this() позволяет конструировать shared_ptr из raw-поинтера.

Класс с родителем enable_shared_from_this() имеет управляющую структуру для shared_ptr в самом объекте класса (что и позволяет ему обращаться к ней изнутри объект этого самого класса), тогда как конструктор из raw указателя выгляди как shared_ptr( raw_ptr ).

В строчке кода c = nullptr; вы записываете в переменную с указатель на новый объект, и тут как раз и вызывается конструктор shared_ptr( raw_ptr ), а перед этим, естественно, вызывается деструктор у предыдущего объекта

при вызове любого метода нулевым невидимым параметром будет передан this

Да, в общем случае все так. Но это никак не связано с enable_shared_from_this(). Просто у вас ошибка в коде. В методе myMethod() вы удаляете глобальный объект, к которому потом пытаетесь обратиться в функции main()

Класс с родителем enable_shared_from_this() имеет управляющую структуру для shared_ptr в самом объекте класса (что и позволяет ему обращаться к ней изнутри объект этого самого класса), тогда как конструктор из raw указателя выгляди как shared_ptr( raw_ptr )

В моей реализации Card DOM в этой статье все узлы DOM-а наследуют у enable_shared_from_thisи используют этот факт, чтобы создавать weak_ptr и shred_ptr внутри своих методов. То есть я использую этот базовый класс именно так как у вас описано.

Наше разногласие в другом в другом:

Я: Даже если мы передаем параметры в функции и храним локальные ссылки как shared_ptr, мы все равно рискуем, потому что this все равно передается как T*

YogMuskrat: Разве не получится избежать этого через std::enable_shared_from_this ?

Я: Вот минимальный код, который показывает, что простое наследование от enable_shared_from_this не решает проблему передачи this как T*.

Вы согласны, что enable_shared_from_this не решает проблему передачи this как T*?
Если нет, приведите код.

Даже если мы передаем параметры в функции и храним локальные ссылки как shared_ptr, мы все равно рискуем, потому что this все равно передается как T*

enable_shared_from_this решает только одну задачу, получение доступа к управляющей структуре shred_ptr изнутри объекта класса. Больше ни для чего другого этот шаблоне не предназначен и не передает ничего и никуда.

shred_ptr и this - это совершенно разные сущности не связанные между собой (точнее shred_ptr хранит указатель, но это может быть любой указатель, не обязательно на объект).

Вы согласились со мной, что enable_shared_from_this не решает проблему передачи this как T* и тут же поставили мне минус. Наверное нам стоит прекратить общение.

Вы читаете что вам пишут? enable_shared_from_this не решает проблему передачи this как T*, так как он не предназначен для этого.

Минус я вам не ставил, а наоборот, давно плюсанул в карму (хотя с таким подходом к общению наверно скоро действительно дойдет и до минуса).

В строчке кода c = nullptr; вы записываете в переменную с указатель на новый объект, и тут как раз и вызывается конструктор shared_ptr( raw_ptr ), а перед этим, естественно, вызывается деструктор у предыдущего объекта

  1. Я не записываю в этой строчке в переменную с указатель на новый объект.

  2. здесь не вызывается shared_ptr( raw_ptr ) здесь не вызывается этот конструктор, и даже если бы тут присваивался не nullptrтут бы все равно вызывался operator=, а не конструктор.

  3. деструктор вызывается как часть оператора= после конструирования нового объекта, т.к. сконструирвоанный объект - параметр функции operator=, а по стандарту вычисление параметров выполняется до выполнения функции, даже при инлайне.

В методе myMethod() вы удаляете глобальный объект, к которому потом пытаетесь обратиться в функции main()

Покажите строчку в функции mainв которой происходит обращение к удаленному объекту.

  1. Видите, здесь вызывается shared_ptr(nullptr_t) а не shared_ptr( raw_ptr ).

  2. Далее вызывается деструктор не старого объекта а временного null-shared-ptr-а

  3. В этой строчке c->myMethod(); объект еще не удален.

  4. Не используйте вывод конкретного компилятора для конкретной платформы с конкретными ключами компиляции. Стандарт разрешает не создавать здесь временных объектов, и в не-дебаг режиме они с огромной вероятностью не будут создаваться.

Видите, здесь вызывается shared_ptr(nullptr_t) а не shared_ptr( raw_ptr ).

Потому что nullptr_t в С++ отдельный тип и вызывается именно этот конструктор, а не shared_ptr( 0 ).

В этой строчке c->myMethod(); объект еще не удален.

Согласен, при вызове метода, объект еще существует. Он удаляется во время вызова.

Не используйте вывод конкретного компилятора ....

Как еще можно продемонстрировать создаваемый код, если не с помощью вывода компилятора?

Возникает несколько основополагающих вопросов:
1. Гарантия последовательности вызовов конструкторов и аллокаторов с аргументами
2. Наличие скрытых флагов, определяемых компиляторов, помимо полей классов и таблиц виртуальных функций ВФ
3. Гарантия одновременной аллокации таблиц ВФ, флагов вместе с объектом/иерархией
4. Отсутствие проблемы чтение-модификация-запись для флагов объекта через асинхронные процедуры (указатель снабжается мьютексом)
5. Управление через индексацию [i] или список prev-next для неявных механизмов
6. Управление размерностью счётчиков ссылок и другими атрибутами встраиваемыми в класс
7. Выравнивание объектов для возможной реализации в виде кастомного аллокатора в пуле объектов или использование готовых специализированных пулов
8. Управление графом объектов с множественными циклическими ссылками, при этом, существуют shared-объекты со стековым удалением (например, помечаемые флагом), в этом случае один объект-атрибут в байт может отложить деаллокацию гигабайта временных копий.

Слишком тезисно и слишком оторвано от темы DOM-подобных структур данных в C++. Позвольте процитировать классика: "Какой заяц? Какой орёл?"

Как раз указаны основные проблемы характерные (не только для С++) для синтаксических деревьев, получаемых после парсинга HTML/CSS и других языков разметки-структурирования (включая XML/JSON). Всё колдовство вокруг итераторов - это сэкономить несколько строчек и выразить лаконичный обход по дереву, особенно что касается prev[N]-next[M] (множественные ссылки и кольцевые связи) и одновременного наличия атрибутов [0,1,2,...]->... Этот микс никогда не был эффективным за исключением хеш-таблиц. Копирование объектов и ссылок для определённого вида эффективнее (но хуже по объёму) в виде линейных массивов с секторами объектов, идентификаторами и смещением, включая матрицы инцидентности, но это уже изначально архитектурные изыскания с точки зрения оптимизации и написания в объектах своих микро-new() и delete(), которые не используют системные аллокаторы, включая наличие скрытых флажков в классе (мьютексы и пометка на удаление) и счётчиков ссылок. Smart pointer - это попытка универсальными (по большей мере не языковыми) методами закрыть сугубо специализированную проблему с не оптимальной (и не безопасной) привязкой к системным аллокаторам, вызывающих утечку вне сегмента, в простейших случаях это обладает эффективностью во всех остальных - необходимо уже учитывать то что написано в пунктах, но это уже не Smart Pointer в том виде в котором его предлагает Stdlib а, грубо говоря, полноценный Asyncio между объектами с очередями/семафорами, стеком удаления, флагами управления состоянием объекта (команды сборщику мусора) включая кучу итд.

  1. У вас есть на примете язык/фреймворк который может эффективно реализовать задачу Card DOM (и вообще хэндлинг DOM-стурктур)?

  2. Вы можете на нем реализовать эту задачу, чтобы показать этот язык/технологию с лучшей стороны?

  3. Так же было бы неплохо провести gap-analysis, показав в чем преимущества и чем приходится ради этого пожертвовать.

Ваша серия статей меня заинтересовала и я сейчас делаю реализацию DOM структуры на базе одной библиотеки для безопасной работы с памятью. Но у меня есть несколько вопросов по требованиям (ограничениям) для DOM объекта.

Контроль единственности владельца. Карточка не может быть вставлена в несколько документов одновременно и не может быть вставлена в один и тот же документ несколько раз. Аналогично с элементами карточек — они должны иметь строго одного владельца — или карточку или группу.

Почему накладывается такое ограничение? Почему не может быть двух владельцев у одного объекта?

Стили и битмапы — неизменяемые ресурсы, разделяемые между текстовыми блоками и картинками. Любое их изменение должно выполняться через copy-on-write.

Так не изменяемые или изменяемые через copy-on-write? Это принципиально различные требования, которые требуют совершенно разной реализации в коде (особенно при работе в нескольких потоках).

Очень упрощённо, это реализуется на простом С как:
- битовое поле с прагмами aligned/pack с атрибутом volatile содержащее флаг пометки объекта на удаление и/или в процессе удаления (чтобы избежать цикла) в двусвязном списке, там же счётчик ссылок на объект
- копируемый массив ссылок вместо самих объектов. Как то рисовал это в аски-графике. Идея в том чтобы при удалении переносить из одного объекта в другой массив указателей до опустошения

- чтобы избежать проблему гонки данных и чтение-модификация-запись использовать протокол обмена данными между объектами на основе запрос-обработка-ответ в различных переменных (с гарантией отсутствия одновременного доступа, volatile 32/64 бита) на основе флажков

-имеется вектор указателей на большие объекты, а сами они расположены в линейной памяти по порядку. Размер объектов 4/8/16/32... слов. Строки и текстовые литералы - в отдельном массиве с аллокацией системой.
- системные malloc/free (new-delete) только для указанных выше кластеров объектов. Внутри объекта указатель заменяется на ID, равный смещению в массиве. В случае потери ссылки или битом объекте - высвобождается кластер, исключение возникает если ID за пределами или внутри удалённого кластера.

-искусственная таблица виртуальных функций с заданным размещением - вызов обработчиков по указателю, в этом случае напрямую проверяется ClassID и управление становится более прозрачным, так как идентификатор класса есть уже протокол. Наподобие тому как раскидывать данные по объектам в иерархии прилетающие по RS-485.

DOM - это уже не просто ООП/итераторы а протокол согласования и обмена данными между объектами, очень грубо это TCP/IP между элементами, включая даже таймауты, когда ждём событие от кнопки, тормозящее всё остальное. Или деструктор, зависший на ожидании исключения при внезапно отпавшей сети.

Иными словами, для решения этой проблемы должен быть некий Python/Perl генератор, который формирует код хоть на ассемблере, но без костылей и с прозрачными механизмами, на входе - JSON с описанием того что должно быть в этой модели. Классическими языками это не победить в должной мере (быстро, компактно, не дорого, нужное подчеркнуть)


Во всех остальных случаях - это бодание с системными функциями и механизмами, реализующими SmartPointer, в итоге удобство превращается в довольно обвесистый код, содержащий борьбу с абстракциями вместо прямого решения проблемы. Как только видится dynamic/static/realloc и прочий cast, включая typeid и прочие внутренности класса, рушится сама концепция инкапсуляции и целостности объектов, этот трюк только для интерпретируемых языков.

Немного неверно используется move(). Пример.

Возможно и тесты где посыпятся, если все поправить, ведь сейчас все просто копируется =)

Остальное потом досмотрю.

Не совсем.

Если у вас есть поле типа T, в котором определен move-конструктор, и у вашего класса есть конструктор, прямо инициализирующий это поле из своего параметра, самый эффективный способ передать этот параметр - по не-констрантному значению с последующим move-ом. Передача по значению позволяет вызывать конструктор как с lvalue, так и с rvalue. Внутри конструктора параметр - это ваша собственная локальная копия, поэтому перемещать из неё всегда безопасно.

Почему следует избегать варианта с передачей по не-константной ссылке:

  • Приводит к неожиданному и всегда нежелательному изменению объектов вызывающей стороны.

  • Не может привязываться к rvalue - такой конструктор не может вызываться с временным объектом, фактически выпадая из композиции функций.

Это подробно обсуждалось Скоттом Мейерсом в Effective C++11 14 Sampler.

Я то тоже читал Мейерса. Там просто несколько перегрузок писалось, насколько помню.

Я не понимаю, что у Вас в коде должен делать move() и в каких случаях. По-моему сейчас он ничего не делает.

Проверил код, во всех случаях move переносит локальные объекты со стека в хип, в поля объектов и элементы векторов. Для строк это позволяет убрать все аллокации и сократить объем кода до пары sse/avx инструкций, а при муве смарт-поинтеров полностью исключить работу с атомиками. "Много перегрузок" не надо, если передавать по не-константному значению и потом мувать. Есть еще оптимизация с &&-ссылками появившаяся в `17, но она тут не нужна.

Ну не знаю. Надо будет перечитать. А лучше засуну кусочками в годболт.

Sign up to leave a comment.

Articles