Какой самый безопасный способ хранить объекты классов, производных от общего интерфейса, в общем контейнере?

Я хотел бы управлять кучей объектов классов, полученных из класса общего интерфейса в общем контейнере.

Чтобы проиллюстрировать проблему, предположим, что я создаю игру, в которой будут разные актеры. Назовем интерфейс 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, dynamic_cast), но я не могу решить, какой из них выбрать.

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

Помощь очень ценится :).


person svenstaro    schedule 22.03.2010    source источник


Ответы (4)


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

Причина этого в том, что контейнер 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));
person Martin York    schedule 22.03.2010
comment
понравилось, как вы использовали for_each - person Yogesh Arora; 22.03.2010
comment
Можете ли вы объяснить свой for_each и как бы вы использовали его, используя BOOST_FOREACH? - person svenstaro; 23.03.2010
comment
std::for_each(I1,I2,Действие). Применяет действие ко всем значениям (в данном случае вызывает метод updateDraw) в диапазоне от I1 до I2 (не включая I2). Где I1, I2 — итераторы. См.: sgi.com/tech/stl/for_each.html. - person Martin York; 23.03.2010
comment
Вы должны задать другой вопрос о том, как использовать BOOST_FOREACH(). Я не использую его достаточно, чтобы ответить на первый вопрос, и в данный момент у меня нет компилятора, с которым можно было бы поиграть. - person Martin York; 23.03.2010

Чтобы решить проблему, которую вы упомянули, хотя вы идете в правильном направлении, но вы делаете это в неправильном направлении. Это то, что вам нужно сделать

  • Определите базовый класс (который вы уже делаете) с виртуальными функциями, которые в вашем случае будут переопределены производными классами Enemy и Civilian.
  • You need to choose a proper container with will store your object. You have taken a std::vector<IActor> which is not a good choice because
    • Firstly when you are adding objects to the vector it is leading to object slicing. This means that only the IActor part of Enemy or Civilian is being stored instead of the whole object.
    • Во-вторых, вам нужно вызывать функции в зависимости от типа объекта (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();
     }
}

Что очень похоже на ваш исходный код, за исключением части интеллектуальных указателей.

person Yogesh Arora    schedule 22.03.2010
comment
Мне нравится ваш подход, так как он выглядит очень чистым для подхода, основанного на указателях. Однако, как указывали другие, то, что я хочу сделать, вероятно, лучше сделать с помощью контейнеров Boost Pointer, поскольку они действительно кажутся созданными именно для того, чего я хочу достичь. Я попробую ваш подход, если это не удастся. Или вы видите причину не использовать контейнеры Boost Pointer? - person svenstaro; 23.03.2010
comment
Даже я думаю, что подход Boost Pointer Containers лучше и синтаксически проще в использовании. - person Yogesh Arora; 23.03.2010
comment
Однако контейнеры Boost Pointer не могут использоваться в некоторых алгоритмах STL boost.org/doc/libs/1_42_0/libs/ptr_container/doc/ - person Yogesh Arora; 23.03.2010

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

person Steve314    schedule 22.03.2010

Если вы хотите, чтобы контейнер эксклюзивно владел элементами в нем, используйте контейнер указателя Boost: они предназначены для этой работы. В противном случае используйте контейнер shared_ptr<IActor> (и, конечно, используйте их правильно, а это означает, что все, кому нужно разделить владение, используют shared_ptr).

В обоих случаях убедитесь, что деструктор IActor является виртуальным.

void* требует от вас ручного управления памятью, так что это исключено. Boost.Any является излишним, когда типы связаны наследованием - стандартный полиморфизм делает свою работу.

Нужен ли вам dynamic_cast или нет, является ортогональной проблемой - если пользователям контейнера нужен только интерфейс IActor, и вы либо (а) делаете все функции интерфейса виртуальными, либо (б) используете идиому невиртуального интерфейса , тогда вам не нужен dynamic_cast. Если пользователи контейнера знают, что некоторые из объектов IActor являются «действительно» гражданскими, и хотят использовать вещи, которые есть в интерфейсе Civilian, но не в IActor, тогда вам понадобятся приведения (или редизайн).

person Steve Jessop    schedule 22.03.2010