Какой самый безопасный способ хранить объекты классов, производных от общего интерфейса, в общем контейнере?
-
21-09-2019 - |
Вопрос
Я хотел бы управлять кучей объектов классов, производных от класса общего интерфейса, в общем контейнере.
Чтобы проиллюстрировать проблему, предположим, что я создаю игру, в которой будут разные актеры.Назовем интерфейс IActor
и вывести Enemy
и Civilian
от него.
Теперь идея состоит в том, чтобы основной цикл моей игры мог это сделать:
// somewhere during init
std::vector<IActor> ActorList;
Enemy EvilGuy;
Civilian CoolGuy;
ActorList.push_back(EvilGuy);
ActorList.push_back(CoolGuy);
и
// main loop
while(!done) {
BOOST_FOREACH(IActor CurrentActor, ActorList) {
CurrentActor.Update();
CurrentActor.Draw();
}
}
...Или что-то вдоль этих линий.Этот пример, очевидно, не будет работать, но именно поэтому я и спрашиваю здесь.
Я хотел бы знать:Каким будет лучший, самый безопасный и высокоуровневый способ управления этими объектами в общем гетерогенном контейнере?Я знаю множество подходов (Boost::Any, void*, класс-обработчик с boost::shared_ptr, Boost.Pointer Container, Dynamic_cast), но я не могу решить, какой из них лучше использовать.
Также я хотел бы подчеркнуть, что хочу держаться подальше от ручного управления памятью или вложенных указателей.
Помощь очень ценится :).
Решение
Как вы уже догадались, вам нужно хранить объекты в виде указателей.
Я предпочитаю использовать контейнеры указателей повышения (а не обычный контейнер интеллектуальных указателей).
Причиной этого является то, что контейнер boost ptr обращается к объектам, как если бы они были объектами (возвращающими ссылки), а не указателями.Это упрощает использование стандартных функторов и алгоритмов в контейнерах.
Недостаток интеллектуальных указателей заключается в том, что вы разделяете право собственности.
Это не то, чего вы действительно хотите.Вы хотите, чтобы право собственности находилось в одном месте (в данном случае в контейнере).
boost::ptr_vector<IActor> ActorList;
ActorList.push_back(new Enemy());
ActorList.push_back(new Civilian());
и
std::for_each(ActorList.begin(),
ActorList.end(),
std::mem_fun_ref(&IActor::updateDraw));
Другие советы
Чтобы решить проблему, о которой вы упомянули, вы хоть и идете в правильном направлении, но делаете это не так.Это то, что вам нужно будет сделать
- Определите базовый класс (что вы уже делаете) с виртуальными функциями, которые будут переопределены производными классами.
Enemy
иCivilian
в твоем случае. - Вам нужно выбрать подходящий контейнер, в котором будет храниться ваш объект.Вы взяли
std::vector<IActor>
это не лучший выбор, потому что- Во-первых, когда вы добавляете объекты в вектор, это приводит к нарезке объектов.Это означает, что только
IActor
частьEnemy
илиCivilian
хранится вместо всего объекта. - Во-вторых, вам нужно вызывать функции в зависимости от типа объекта (
virtual functions
), что может произойти только в том случае, если вы используете указатели.
- Во-первых, когда вы добавляете объекты в вектор, это приводит к нарезке объектов.Это означает, что только
Обе причины выше указывают на то, что вам нужно использовать контейнер, который может содержать указатели, что-то вроде std::vector<IActor*>
.Но лучшим выбором было бы использовать container of smart pointers
что избавит вас от головной боли по управлению памятью.Вы можете использовать любые интеллектуальные указатели в зависимости от ваших потребностей (но не auto_ptr
)
Вот как будет выглядеть ваш код
// somewhere during init
std::vector<some_smart_ptr<IActor> > ActorList;
ActorList.push_back(some_smart_ptr(new Enemy()));
ActorList.push_back(some_smart_ptr(new Civilian()));
и
// main loop
while(!done)
{
BOOST_FOREACH(some_smart_ptr<IActor> CurrentActor, ActorList)
{
CurrentActor->Update();
CurrentActor->Draw();
}
}
Это очень похоже на ваш исходный код, за исключением части интеллектуальных указателей.
Моя мгновенная реакция такова: вам следует хранить интеллектуальные указатели в контейнере и убедиться, что базовый класс определяет достаточное количество (чистых) виртуальных методов, которые вам никогда не понадобятся. dynamic_cast
вернемся к производному классу.
Если вы хотите, чтобы контейнер единолично владел элементами в нем, используйте контейнер-указатель Boost:они созданы для этой работы.В противном случае используйте контейнер с shared_ptr<IActor>
(и, конечно, используйте их правильно, то есть каждый, кому необходимо разделить собственность, использует shared_ptr
).
В обоих случаях убедитесь, что деструктор IActor
является виртуальным.
void*
требует от вас ручного управления памятью, так что это невозможно.Boost.Any является излишним, когда типы связаны наследованием — стандартный полиморфизм делает эту работу.
Нужен ли вам dynamic_cast
или нет, является ортогональной проблемой: если пользователям контейнера нужен только интерфейс IActor, и вы либо (а) делаете все функции интерфейса виртуальными, либо (б) используете идиому невиртуального интерфейса, тогда вы не мне не нужен динамический_cast.Если пользователи контейнера знают, что некоторые из объектов IActor «на самом деле» являются гражданскими лицами, и хотят использовать вещи, которые есть в интерфейсе Civilian, но не в IActor, тогда вам потребуется приведение типов (или перепроектирование).