Gamedev
Ambushed Raccoon
3357

C++ Идеальная статическая иерархия или как объединить объекты в структуру

Доброго времени суток. Представлюсь, для тех, кто меня еще не знает. Меня зовут Дима. Я работаю C++ разработчиком уже более 5-ти лет. На данный момент работаю в крупной Gamedev-студии. Помимо работы увлекаюсь созданием образовательного контента для YouTube и Twitch каналов.

В закладки

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

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

Геймдев. Игры жанра Match3. На момент запуска этого приложения Вы уже знаете, что Вам от него нужно. Главное меню, уровень. Уровень может конфигурироваться файлами с вашего диска или информацией с сервера, но вы уже знаете все объекты, которые вам нужно создать. Соответственно, не нужно думать, что и в какой момент выделить, что и когда удалить. Всё, что вам нужно, – правильное отображение информации. Можно привести в пример даже AAA-игры. Запускаем очередную версию Battlefield и видим, что, отображая только главное меню, игра уже выделила 4 Гб оперативной памяти. Подозрительно? Да нет, не очень. Очевидно, что разработчики следовали тому же принципу.

Симуляция. Мне приходилось работать с программными продуктами, назначение которых – симулировать процессы, происходящие на железе. ТО есть симулировать некий микроконтроллер или нечто более сложное. Жёсткий диск, процессор. Зачем это нужно? Для тестирования прошивки железа или драйверов, когда самого железа еще не существует, или устройств слишком мало, чтобы дать возможность тестировать его достаточному количеству разработчиков прошивки. По сути, винчестер не изменится в процессе работы, в нём не станет больше памяти или один контроллер вдруг не заменит другой. Соответственно, имеет смысл создать все объекты сразу на момент запуска теста и удалить их на момент его завершения.

Далее всё довольно просто. Вы создаёте некий главный объект. Например, Game. В его полях вы создаёте объекты MainMenu, Level, ScoreBoard, NetworkManager... В их полях создаёте другие объекты и так далее. Всё вроде бы хорошо. Однако ваша архитектура усложняется, вам необходимы какие-то общие методы, всё-таки у нас тут ООП и полиморфизм, например, вызов какой-то логики у объекта, каждый кадр, тот, кто работал с игровыми движками, понимает. Или же, как минимум, объектам понадобятся общие методы для поиска друг друга. И тут возникает проблема, ибо нужно все объекты объединить в некую абстрактную иерархию базовых объектов.

Я сталкивался с решениями, когда для объектов дочерних (не в смысле наследников, а в смысле полей класса) нужно вызвать что-то типа add_child и добавлять этот вызов самостоятельно для каждого созданного объекта. Мне кажется, что это чересчур опасное решение с точки зрения вероятности упустить добавление объекта, особенно человеку, который не знаком с правилами.

Цель

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

class Game { public: Player _player; Enemy _enemy; }

В приведённом примере мы хотим иметь возможность обратится к полям _player или _enemy класса Game через некий массив указателей _children;

Решение

Очевидно, что нам понадобится некий базовый класс:

class BaseNode { public: BaseNode(const std::string& name); protected: //Добавим каждому объекту возможность идентифицировать себя std::string _name; std::vector<BaseNode*> _children; };

Далее разберёмся с порядком вызова конструкторов. Порядок вызова конструкторов довольно прост. Порядок вызова = порядок объявления.
Продемонстрирую на примере:

class Base {}; class Field : public Base {}; class Child : public Base { Field _field1; Field _field2; };
  • Base() - класс Child
  • Base() - поле _field1
  • Fileld() - поле _field1
  • Base() - поле _field2
  • Fileld() - поле _field2
  • Child() - класс Child

Обратимся к написанному ранее классу BaseNode. По сути будет всегда запускаться следующая цепочка. BaseNode контейнера, BaseNode полей, конструкторы полей, конструктор контейнера. Здесь уже можно обратить внимание, что это всё напоминает поиск в глубину.

Создадим глобальный стек.

namespace { std::stack<BaseNode*> stack; } BaseNode::BaseNode(const std::string& name) : _name(name.) { if (!stack.empty()) { //на вершине стека находится объект-контейнер текущего объекта stack.top()->_children.push_back(this); } //Добавляем себя на вершину стека stack.push(this); }

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

  • Base() - класс Child
  • Base() - поле _field1
  • Fileld() - поле _field1. В этой точке уже известно, что детей у field1 больше нет и мы можем удалять его с вершины стека.

Чтобы добиться нормального удаления объекта с вершины стека, не добавляя удаление в каждый новый конструктор, вместо const std::string& name в конструкторе нашего класса будем использовать свой собственный временный объект, который будет отвечать за индикацию классов по имени.

struct NodeName { NodeName(const char* name); NodeName(const std::string& name); ~NodeName(); std::string _name; };

Каким будет время жизни данного объекта? Мы по-прежнему будем объявлять его в конструкторах через константную ссылку, ибо объявление по значению для объектов, которые больше по своему размеру, чем системное слово, как-то некрасиво. Но, по сути, нам нужен именно такой объект, дабы добиться его времени жизни только на тот момент, пока мы находимся в цепочке конструкторов объекта-контейнера. То есть "пытаясь" вызвать конструктор контейнера, мы попадём в базовый конструктор, затем конструкторы детей, а потом уже в конструктор контейнера, по выходу из которого NodeName и будет уничтожен. Получается, что удалять объект с вершины глобального стека нужно как раз на разрушении очередного объекта класса NodeName. Перейдём, наконец, к полной реализации.

// BaseNode.h #pragma once #include <string> #include <vector> #define NODE_DECL(obj) obj{#obj} struct NodeName { NodeName(const char* name); NodeName(const std::string& name); ~NodeName(); std::string _name; }; class BaseNode { public: BaseNode(const NodeName& name); protected: std::string _name; std::vector<BaseNode*> _children; }; ////////////////////////////////////////////////// //BaseNode.cpp #include "BaseNode.h" #include <iostream> #include <stack> namespace { std::stack<BaseNode*> stack; } BaseNode::BaseNode(const NodeName& name) : _name(name._name) { if (!stack.empty()) { //Обратились к вершине стека и добавили туда себя в качестве ребёнка stack.top()->_children.push_back(this); } // Добавили себя на вершину стека stack.push(this); } //Два конструктора для более удобной работы NodeName::NodeName(const char* name) : _name(name) {} NodeName::NodeName(const std::string& name) : _name(name) {} NodeName::~NodeName() { //Снимаем объект BaseNode с вершины стека stack.pop(); }

Можно обратить внимание, что в удалении объекта нет проверки на то, что стек не пуст. Это сделано нарочно, ведь если стек окажется не пуст, значит кто-то построил иерархию неверно и приложение упадёт на этапе инициализации.

#define NODE_DECL(obj) obj{#obj}

Данный макрос будем использовать для инициализации полей классов в нашей иерархии, чтобы создавать NodeName с помощью строкового литерала, то есть приводя к созданию временного объекта. Плюс получится довольно удобно, что имя объекта в иерархии будет равно имени переменной в коде. Продемонстрирую применение на примере:

#include <iostream> #include "BaseNode.h" // Все классы должны быть наследниками BaseNode class C : public BaseNode { public: C(const NodeName& name) : BaseNode(name) {} }; //Данный класс будет хранить один дочерний объект в иерархии class A : public BaseNode { public: A(const NodeName& name) : BaseNode(name) {} private: //Дочерний объект имя дочернего объекта будет "_c" C NODE_DECL(_c); }; //Данный класс будет хранить один дочерний объект в иерархии class B : public BaseNode { public: B(const NodeName& name) : BaseNode(name) {} private: //Дочерний объект C NODE_DECL(_c); }; //Главный класс, который хранит два дочерних объекта классов A B class Parent : public BaseNode { public: Parent(const NodeName& name) : BaseNode(name) {} private: A NODE_DECL(_a); B NODE_DECL(_b); }; int main() { //Объявим данный объект на стека //По заверешнию выполнения функции мэйн он будет удалён //В случае с большим объектами можно создать его на куче //И удалить по заверешнию работы программы Parent NODE_DECL(parent); return 0; }

В итоге, следует выполнять два пункта для работы нашей иерархии:

  • Наследование от базового класса
  • Правильное объявление переменных (для стандартизации предлагаю пользоваться подобными макросами).

Каждый раз объявлять конструктор и вызывать конструктор базового класса тоже выглядит довольно-таки громоздко. Для ускорения можно использовать оператор using:

// Без using class C : public BaseNode { public: C(const NodeName& name) : BaseNode(name) {} }; //Иcпользуя using class A : public BaseNode { public: using BaseNode::BaseNode; };

Заключение

Прелесть данного решения в том, что технический долг, которого нужно придерживаться, создавая новые объекты в иерархии, крайне мал. Также присутствует защита, которая тут же сообщит разработчику-пользователю, что он объявляет объекты в иерархии неверно. Речь об удалении объекта с вершины стека, без проверки на его пустоту.
Задавайте в комментариях вопросы. Пишите, если увидите какие-то ошибки, и спасибо за внимание.

Для полной картины и объяснения в формате видео предлагаю ознакомиться с записью стрима, где я реализую код из статьи и объясняю всё максимально подробно:

Мы на других ресурсах

  • YouTube
  • Twitch
  • Telegram - для извещения о трансляции и появлении новых видео.
{ "author_name": "Ambushed Raccoon", "author_type": "self", "tags": ["pragma","obj","include","define"], "comments": 35, "likes": 51, "favorites": 218, "is_advertisement": false, "subsite_label": "gamedev", "id": 126148, "is_wide": false, "is_ugc": true, "date": "Sun, 19 Apr 2020 11:55:39 +0300", "is_special": false }
0
35 комментариев
Популярные
По порядку
Написать комментарий...
22

Просто эталон бесполезной статьи.

Человек, не знающий языка, не поймёт что ты тут написал.

Человек, знающий язык, поймёт, что ты написал лютый бред, да ещё и крайне плохо.

Получили абсолютно неюзабельный самописный недо-рефлекшн, делающий нормальную разработку невозможной, требующий постоянной поддержки штанов руками (пойди да оберни каждое поле), ну и заодно люто раздувающий любой код как по памяти, так и по времени выполнения.

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

Дружище, если ты хочешь написать хорошую статью по программированию:

1. Определись для кого пишешь. Либо для людей, не знающих про конструкторы, либо для людей, которым зачем-то понадобился твой недо-рефлекшн. Это две разные группы людей.

2. Определись, какую задачу решаешь. Потрать время на объяснение цели, а не оправдание своего письменного стиля:

(да-да, тавтология-повторение)

3. Сконцентрируйся на сути, а не на мусоре. Все поняли уже, что ты не в курсе, как в 2020м году правильно передавать строчки в конструктор, а заодно не понимаешь, что std::string тут изначально не нужен, ну так и не трать время на это. Воткни один любой вариант, напиши "добавьте тут чего хотите", и не копипасть два варианта конструктора пять раз подряд.

4. Самое важное: попроси ревью у коллег. Со всеми бывают задвиги, когда вроде написал что-то, а потом смотришь через два дня и стыдно. В этом нет ничего страшного. Для того и нужно работать в команде, чтоб такая фигня не попадала в продакшн, ну или в твоём случае в статью.

Ответить
0

Если вы хотите дать совет, не стоит начинать комментарий с фразы "эталон бесполезной статьи". но я Вам подыграю.
Эталон бесполезного комментария.
"Человек, знающий язык, поймёт, что ты написал лютый бред, да ещё и крайне плохо." - тоже весьма конструктивно. Данное решение успешно работает и работало на двух проектах, находящихся в продакшне, и оно себя отлично зарекомендовало. Более того, я заметил недостаток отсутствия этого подхода на проекте, работающем по такому же принципу, но без автоматического построения иерархии.
Макрос для объявления поля - не такая уж и большая плата, и иерархия защищена от неправильного построения.
"Я б прошел мимо ничего не сказав, но к сожалению есть риск, что кто-то посередине прочитает статью и подумает, что всё написанное в ней — хорошая идея." - в статье был описан определённый класс задач и архитектур, для которых это хорошая идея.
"так ещё и из каждого надо потом выбивать подобную ересь, которую он где-то в интернетах прочитал." - это конкретно ваша проблема, не стоит всем её вываливать.

Ответить
10

Смотри, ситуация следующая. Компьютеры нынче быстрые, и далеко не всем играм нужна производительность и/или адекватная структура кода. Хороший пример VVVVVV (там выкладывали исходники — внутри лютый ад и калька с ActionScript). Игра — отличная, год — говно полное.

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

Если создавать по несколько строк, возможно с аллокациями, на каждый созданный объект, и упихивать всё это в потоково-небезопасную структуру, не поддерживающую ничего кроме "создал/удалил" by design — это совершенно нормально для твоего проекта, и не вызывает никаких проблем с производительностью, то тут разгадка только одна: твой проект настолько прост и нетребователен, что выбор C++ скорее всего был ошибкой, стоящей денег твоему проекту. Пилил бы игру под Юнити с нормальным рефлекшном, было бы продуктивней и производительней. Не тратил бы время на переизобретение велосипедов, и т.д.

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

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

Автор, почему каждое имя поля — отдельный std::string (и 10000 полей с именем relativeWorldOffset, то ты создашь 10000 одинаковых строчек + гарнир), если все имена и так лежат в data секции бинарника, и при включенном стринг пулинге — даже без дупликатов? "Данное решение успешно работает и работало на двух проектах"

Автор, что там с коллекциями, передачей владения и многопоточностью? "Данное решение успешно работает и работало на двух проектах"

Автор, а раз у каждого класса фиксированная структура, не логично ли хранить топологию один раз per class (например просто через паттерн visitor), чтоб если я обхожу 10000 одинаковых объектов, то вообще ничего не повторялось? "Данное решение успешно работает и работало на двух проектах"

Автор, а как верифицировать, что все поля учтены, и никто не забыл воткнуть твой макрос? "Данное решение успешно работает и работало на двух проектах"

Просто просьба — не учи если сам не разбираешься. Только хуже ведь делаешь.

Ответить
1

Не могу не отреагировать на один момент, потому что тут именно Вы вгоняете людей в заблуждение. Проект в один поток = простой проект и ему не нужен C++. Очень странная попытка ударить по моей компетентности. Видимо Вы не знаете, что бывают ситуации когда проект должен быть однопоточным, если речь, например, идёт о симуляции. И длинных по времени тестах (длиной в несколько суток) и главное, что эти тесты должны быть полностью воспроизводимы каждый запуск. Чего очень сложно достичь в многопоточном приложении. И предваряя ваш вопрос о том, что это за однопоточная симуляция ведь железо работает параллельно и тд. Ну так многопоточность тоже симулировалась используя boost::context. Странно, что Вам это всё нужно объяснять, ведь вы ТАК давно в этой сфере.
Признаю два пробела своей статьи. Ошибкой было использовать std::string. Ну и можно было бы сделать работу со стеком потокобезопасной. Вообще подобная агрессивная полемика больше напоминает разговор за столом о важнейших темах типа политики и у меня нет желания принимать в подобном участие. Если Вы хотели что-то доказать мне, то пенять на мою профнепригодность плохая идея. С чего вообще Вы решили, что я перед Вами оправдываюсь, вы считаете что это плохое решение, однако я с этим не согласен, поскольку решение хорошее не потому, что оно просто работает, а потому что оно хорошо себя зарекомендовало на практике. Видимо, Вы не очень привыкли, когда Вам что-то спокойно пытаются объяснить, а не стрелять тезисами в духе: "Твой софт - говно". Не смотря на всё то, что Вы написали, я считаю, что продемонстрированный мной подход с алгоритмической точки зрения себя более чем оправдывает и подходит для описанных мной кейсов. Да, именно для тех кейсов, когда происходит создал-удалил последовательность, ведь между этими двумя действиями просто не может быть ничего чуть более комплексного.
Свои советы по поводу того, что писать, кому писать и для кого я настоятельно рекомендую Вам оставить при себе. Очень сложно из вашего потока желчи выловить что-то полезное.
На этом я желаю поставить точку в этом споре. Ибо мне не о чем больше с Вами разговаривать.

Ответить
0

Это "идеальная иерархия" если что.

Ответить
3

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

Ответить
17

Спасибо, отличная мотивирующая статья для изучения С# ) 

Ответить
10

За Страуструпа и двор стреляю в упор.

Ответить
12

Честно говоря так и не понял чего вы хотите добиться такой ex-machina? Зачем вообще нужен общий стек объектов?

ЗЫ. Потокобезопасность, как я понял, тут и не предполагается?

Ответить
2

"Зачем вообще нужен общий стек объектов?" - в статье написано. На вершине стека лежит объект, в дети к которому надо добавить текущий объект.
"Потокобезопасность, как я понял, тут и не предполагается?" — в каком контексте тут необходима потокобезопасность? О каких разделяемых ресурсах идёт речь? В данной ситуации можно обернуть только инициализацию "главного" объекта. Однако в рамках статьи это было бы излишним.

Ответить
2

"Зачем вообще нужен общий стек объектов?" - в статье написано.

Я несколько раз перечитал вашу статью, и так и не понял цели всех этих манипуляций.
"Потокобезопасность, как я понял, тут и не предполагается?" — в каком контексте тут необходима потокобезопасность?

У вас есть общий стек объектов. Предполагается, что создание всех объектов, которые его используют, будет происходить в одном потоке?

Ответить
0

"У вас есть общий стек объектов..." - да стоило добавить синхронизацию на стек.
"Я несколько раз перечитал вашу статью, и так и не понял цели всех этих манипуляций." - ну смотрите, я там разбирал порядок вызовов конструкторов, думаю тут Вам объяснять не надо. Больше всего мне порядок вызова конструкторов базовых классов иерархии напоминает рекуррентный обход дерева. То есть мы от корня спускаемся к листьям, то бишь к конструкторам объектов-полей.
получается попав в конструктор мы помещаем себя на вершину стека, попав в другой конструктор(это будет уже объект-поле), обратившись к вершине, стека мы получим указатель на нашего "родителя" в иерархии. Опять же разбирая порядок вызова, я показывал, что в очередном поле, попав непосредственно в конструктор данного поля(или же его класса) Мы должны снять себя с вершины стека, ведь конструкторы всех полей уже были вызваны, а значит "детей" больше не осталось. Как понять, что это уже конструктор непосредственно самого класс-наследника. Для этого мы и заводим временный объекта класса NodeName. Который живёт всю цепочку вызовов конструкторов. ТО есть мы пытаемся вызвать конструктор класса наследника, создаём временный объект, пока мы бегаем по базовым конструкторам и конструкторам полей, Объект NodeName существует. А будет он разрушен по выходу из конструктора класса наследника, ну получается что мы снимаем себя не в момент попадания в конструктор, а на момент выхода из него(конструктора класса-наследника) и для этого во временном объекте в деструкторе NodeName мы снимаем указатель с вершины стека. И стек специально на пусто не проверяется, чтобы приложение упало, если иерархия будет составлена неправильно. 

Ответить
1

Давайте я уточню: я понял алгоритм, я не понял зачем это вообще нужно. Какая задача решается этим алгоритмом?

Ответить
0

А. решается задача объединения всех объектов в единую структуру от большего к меньшему. То есть что нужно написать, чтобы затем можно было добавлять некий объект как поле и он с небольшим оверхедом добавлялся в иерархию. Если исключить передачу имени переменной, то даже и макрос не понадобится. Тем не менее не всем понравилось.

Ответить
6

Очевидно, что разработчики следовали тому же принципу

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

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

Ответить
3

Данную систему можно защитить, выделив место под "главный" объект на куче, а сам указатель хранить на стэке в функции main() например. В принципе, чем меньше глобальных переменных используется, тем сложнее сломать. Ибо глобальные или статические переменные мапятся с одинаковым смещением относительно модуля, в котором они объявлены. Подобные методы взлома работают и в современных играх, но при x64 адресное пространство 8тб - искать сложнее, плюс современные игры гораздо активнее реагируют на вызовы ReadProcessMemory и тому подобное.
Но в целом вы правы, если есть какое место в памяти, где лежит все данные игра будет крайне уязвима. 

Ответить
2

Бахни ECS и не мучайся с ООП.

Ответить
2

Кстати, велосипедирование собственного ECS-фреймворка для compile-time - отличная разминка для мозгов, если хочется темплейты попердолить.

Ответить
12

Обычно темплейты тебя пердолят, а не наоборот, но это уже совсем другая история

Ответить
5

*Съешь яблоко вместо каши, оно вкуснее.

Ответить
2

Спасибо за такое, полезно и интересно!

Ответить
1

Спасибо, дальше изучаю ecmascript

Ответить
1

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

Ответить
1

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

Ответить
0

Можно минимальный пример, а то что-то не догоняю идею, и вообще есть чувство, что говорим о разных вещах.

Ответить
0

Попробую всё разложить ещё раз. Частично скопировано с другого комментария.

 ну смотрите, я там разбирал порядок вызовов конструкторов. Больше всего мне порядок вызова конструкторов базовых классов иерархии напоминает рекуррентный обход дерева. То есть мы от корня спускаемся к листьям, то бишь к конструкторам объектов-полей.
получается попав в конструктор мы помещаем себя на вершину стека, попав в другой конструктор(это будет уже объект-поле), обратившись к вершине стека мы получим указатель на нашего "родителя" в иерархии. Опять же разбирая порядок вызова, я показывал, что в очередном поле, попав непосредственно в конструктор данного поля(или же его класса) Мы должны снять себя с вершины стека, ведь конструкторы всех полей уже были вызваны, а значит "детей" больше не осталось. Как понять, что это уже конструктор непосредственно самого класса-наследника? Для этого мы и заводим временный объекта класса NodeName. Который живёт всю цепочку вызовов конструкторов. То есть мы пытаемся вызвать конструктор класса наследника, создаём временный объект, пока мы бегаем по базовым конструкторам и конструкторам полей, объект NodeName существует. А будет он разрушен по выходу из конструктора класса наследника, ну получается что мы снимаем себя не в момент попадания в конструктор, а на момент выхода из него(конструктора класса-наследника) и для этого во временном объекте в деструкторе NodeName мы снимаем указатель с вершины стека. И стек специально на пустоту не проверяется, чтобы приложение упало, если иерархия будет составлена неправильно. 

Ответить
0

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

Ответить
0

Дело в том, что она только в примере пустая. Просто из-за того, что надо как-то уместить материал в статью. Если речь идёт об играх, то это были какие-то объекты, которые бы имели возможность получать отклики например от движка. 
Плюс добавляется возможность например поиска по иерархии, по полному имени например, чтобы не было "горизонтальных" связей. В общем тут уже в зависимости от того, что делаете)

Ответить
0

Это понятно, но у меня обычно конструктор по умолчанию - и не конструктор вовсе. Может быть специфика работы такая, хз.

Ответить
0

да, реализация шаблонов ТОЛЬКО в хидер-файлах и последствия этого - вообще отдельная тема для разговора

Ну как бы не обязательно

Ответить
0

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

Ответить
0

Если мне не изменяет память (а она может, с С++ работал давно), то вот тут есть проблемка:
struct NodeName

{

//Вот это будет создавать временные объект, копировать значение и удалять, т.к. для string желаемая автором логика не будет работать, и надо что-то вроде std::forward
NodeName(const std::string& name);

//А еще у меня есть подозрения, что на самом деле вызывается вот этот конструктор обычно, а не тот, что выше, из-за логики по умолчанию при приведении типов
NodeName(const char* name);

Но могу ошибаться :D

Ответить
1

std::forward нужен для передачи универсальных ссылок. std::string вполне комфортно копируется и передаются по константным ссылкам. Ибо существовал задолго до появления std::forward(C++11)
C макросом, который я использую для инициализации, только этот конструктор(const char* name) и будет вызываться. std::string оставлен для консистентности, ибо я храню с помощью std::string

Ответить
0

Это какой-то новый, доселе неизведанный, сорт извращений.

Ответить
0

 вектор указателей

наследование

Это даже из Юнити убирают. А ты приволок этого динозавра из начала 2000х?

Ответить

Комментарии

{ "jsPath": "/static/build/dtf.ru/specials/DeliveryCheats/js/all.min.js?v=05.02.2020", "cssPath": "/static/build/dtf.ru/specials/DeliveryCheats/styles/all.min.css?v=05.02.2020", "fontsPath": "https://fonts.googleapis.com/css?family=Roboto+Mono:400,700,700i&subset=cyrillic" }