Это третья статья в серии про DOM-подобные модели данных в различных языках программирования.
В предыдущих частях:

Сегодня мы рассмотрим как с Card DOM справился С++.

Код

Код на С++ почти вдвое компактнее JS-версии (всего ~158 строк), но это все равно слишком много для включения в статью, поэтому он доступен по ссылке: https://www.mycompiler.io/view/G4ypqaNpDml.

Детали реализации

  • Мы используем weak_ptr для перекрестных ссылок.

  • Мы можем полагаться на автоматическую очистку объектов и обрыв перекрестных ссылок с помощью деструкторов.

  • Хотя узлы нашего дерева имеют ровно одного владельца, мы не можем использовать unique_ptr поскольку на них могут ссылаться weak_ptr.  поэтому все владеющие ссылки в нашем DOM будут shared_ptr.

  • Мы не можем использовать конструкторы копий для создания топологически-корректных копии объектных иерархии, поскольку такое копирование - двухпроходный процесс, он требует формирования и использования map(оригинал->копия). Поэтому мы реализуем глубокое копирование вручную.

  • Поскольку мы используем shared_ptr для представления всех владеющих указателей нам придется вручную детектировать наличие циклов и множественных владельцев в графе владения.

  • Мы можем использовать shared_ptr<const T> для представления разделяемых ресурсов. Это не даст стопроцентной гарантии неизменяемости, но может помочь поймать некоторые ошибки на стадии компиляции.

  • Когда мы удаляем объект из DOM, на него могут указывать raw-указатели из стека. Даже если мы передаем параметры в функции и храним локальные ссылки как shared_ptr<T>, мы все равно рискуем, потому что this все равно передается как T*. Требуется или предельная аккуратность при кодировании или следование правилу, когда всякий вызов метода производится по временно созданному shared_ptr<T>, который будет предотвращать случайное удаление объекта.


Примеры использования

Создание документа

auto doc = make_shared<Document>();
{
    auto style = make_shared<const Style>("Times", 16.5, 600);
    auto card = make_shared<Card>();
    auto hello = make_shared<TextItem>("Hello", style);
    auto button = make_shared<ButtonItem>("Click me", card);
    auto conn = make_shared<ConnectorItem>(hello, button);
    card->add_item(move(hello));
    card->add_item(move(button));
    card->add_item(move(conn));
    card->add_item(make_shared<GroupItem>());
    doc->add_item(move(card));
}
Начальное состояние как в JS-серсии (синее-изменяемое состояние, красное - неизменяемое, зеленое-невладеющие ссылки)
Начальное состояние как в JS-серсии (синее-изменяемое состояние, красное - неизменяемое, зеленое-невладеющие ссылки)

Разделяемые ресурсы: clone-on-mutation

auto hello_text = dynamic_pointer_cast<TextItem>(doc->items[0]->items[0]);

// Попытка изменить замороженный стиль
hello_text->style->size++;  // Ошибка компиляции

// Изменение через копию
auto new_style = hello_text->style->clone();
new_style->size++;
hello_text->style = new_style;

В этом коде, объект, который уже считается разделяемым, все еще доступен по не-константной ссылке new_style

Продление времени жизни с помощью стековой ссылки

{
    auto hello_text = dynamic_pointer_cast<TextItem>(doc->items[0]->items[0]);
    doc->items[0]->remove_item(hello_text);

    // hello_text всё ещё жив и доступен по перекрестной ссылке from из коннектора 
    assert(!dynamic_pointer_cast<ConnectorItem>(
        doc->items[0]->items[1])->from.expired());

} // Удаление происходит здесь

Глубокое копирование с сохранением топологии

auto new_doc = deep_copy(doc);

// Убеждаемся, что в копии перекрестные ссылки показывают так же, как в оригинале
assert(new_doc->items[0]->items[0] ==
    dynamic_pointer_cast<ConnectorItem>(
        new_doc->items[0]->items[1])->to.lock());

assert(new_doc->items[0] ==
    dynamic_pointer_cast<ButtonItem>(
        new_doc->items[0]->items[0])->target_card.lock());

Защита от мульти-владения

try {
    doc->add_item(new_doc->items[0]);
} catch (std::runtime_error&) {
    std::cout << "multiparented!\n";
}

Предотвращение циклов

try {
    auto group = make_shared<GroupItem>();
    auto subgroup = make_shared<GroupItem>();
    group->add_item(subgroup);
    subgroup->add_item(group);

} catch (std::runtime_error&) {
    std::cout << "loop\n";
}

Оценка: как C++ справился с DOM-подобными структурами

Критерий

Да

Но

Безопасность памяти

Умные указатели помогают.

С++ - не дает гарантий безопасности.

Предотвращение утечек

Умные указатели и RAII помогают бороться с утечками.

С++ не предотвращает утечки.

Ясность владения

Разделение shared_ptr и weak_ptr задаёт декларативные границы владения.

За уникальностью владения приходится следить вручную. unique_ptr тут неприменим.

Копирование

Полностью ручная реализация.

Слабые ссылки

Обрываются автоматически, требуют проверки перед использованием.

lock() возвращает shared_ptr, поэтому неосторожное использование может привести к закольцовкам в графе владения и утечкам.

Устойчивость

С++ - небезопасный язык. При работе со структурами данных, даже с использованием умных указателей требуется дисциплина и аккуратность.

Выразительность

Код многословен: make_shared<GroupItem>(),dynamic_pointer_cast<ButtonItem>(t)

Момент обнаружения ошибок

Проверки неизменяемости разделяемых ресурсов производятся при компиляции благодаря shared_ptr<const T>

Проверки закольцовок и множественного владения - происходят только во время исполнения. Тесты обязательны. Много тестов.

Итог: C++ дает контроль, но требует дисциплины, помогает но ничего не гарантирует

C++ предоставляет тонкий контроль над временем жизни и владением,
позволяя построить DOM-подобную структуру с относительно безопасными ссылками и минимальными утечками.

Однако отсутствие встроенной защиты памяти требует избегания сырых указателей и жесткой дисциплины при использовании умных указателей.

В приложении к DOM-подобным структурам язык когнитивно сложен и не безопасен.

Комментарии (33)


  1. YogMuskrat
    18.10.2025 21:04

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

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


    1. kotan-11 Автор
      18.10.2025 21:04

      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


      1. Sazonov
        18.10.2025 21:04

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


        1. kotan-11 Автор
          18.10.2025 21:04

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


          1. Sazonov
            18.10.2025 21:04

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


            1. kotan-11 Автор
              18.10.2025 21:04

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


              1. Sazonov
                18.10.2025 21:04

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

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


                1. kotan-11 Автор
                  18.10.2025 21:04

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

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


                  1. Sazonov
                    18.10.2025 21:04

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


                    1. kotan-11 Автор
                      18.10.2025 21:04

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


                      1. Sazonov
                        18.10.2025 21:04

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


      1. Cfyz
        18.10.2025 21:04

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

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


        1. kotan-11 Автор
          18.10.2025 21:04

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

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


          1. Cfyz
            18.10.2025 21:04

            Вполне допускаю, что такой случай мог произойти, при наслоении разных библиотек и легаси в 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(), или никак.


            1. kotan-11 Автор
              18.10.2025 21:04

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


      1. YogMuskrat
        18.10.2025 21:04

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

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

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


        1. kotan-11 Автор
          18.10.2025 21:04

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


          1. YogMuskrat
            18.10.2025 21:04

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

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


      1. rsashka
        18.10.2025 21:04

        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()


        1. kotan-11 Автор
          18.10.2025 21:04

          Класс с родителем 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*?
          Если нет, приведите код.


          1. rsashka
            18.10.2025 21:04

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

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

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


            1. kotan-11 Автор
              18.10.2025 21:04

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


              1. rsashka
                18.10.2025 21:04

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

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


        1. kotan-11 Автор
          18.10.2025 21:04

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

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

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

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

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

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


          1. rsashka
            18.10.2025 21:04

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

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

            c->myMethod();


            1. kotan-11 Автор
              18.10.2025 21:04

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

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

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

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


              1. rsashka
                18.10.2025 21:04

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

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

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

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

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

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


  1. TimurZhoraev
    18.10.2025 21:04

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


    1. kotan-11 Автор
      18.10.2025 21:04

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


      1. TimurZhoraev
        18.10.2025 21:04

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


        1. kotan-11 Автор
          18.10.2025 21:04

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

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

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


          1. rsashka
            18.10.2025 21:04

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

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

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

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

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


          1. TimurZhoraev
            18.10.2025 21:04

            Очень упрощённо, это реализуется на простом С как:
            - битовое поле с прагмами 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 и прочие внутренности класса, рушится сама концепция инкапсуляции и целостности объектов, этот трюк только для интерпретируемых языков.