поместить другой класс в иерархию в одном контейнере в C ++ - PullRequest
1 голос
/ 14 февраля 2010

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

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

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

vector<vehicle> parking_lot;
vector<*vehicle> parking_lot;

Кто-нибудь может предложить некоторые основные правила для такого рода вопросов?

Ответы [ 5 ]

5 голосов
/ 14 февраля 2010

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

  1. Создайте класс для каждого типа элемента и сделайте так, чтобы они все унаследовали от общей базы и сохранили полиморфные базовые указатели в std::vector<boost::shared_ptr<Base> >. Вероятно, это более общее и гибкое решение:

    struct Shape {
        ...
    };
    struct Point : public Shape { 
        ...
    };
    struct Circle : public Shape { 
        ...
    };
    std::vector<boost::shared_ptr<Shape> > shapes;    
    shapes.push_back(new Point(...));
    shapes.push_back(new Circle(...));
    shapes.front()->draw(); // virtual call
    
  2. То же, что (1), но хранить полиморфные указатели в boost::ptr_vector<Base>. Это немного менее общее, потому что элементы принадлежат исключительно вектору, но этого должно быть достаточно в большинстве случаев. Одним из преимуществ boost::ptr_vector является то, что он имеет интерфейс std::vector<Base> (без *), поэтому его проще использовать.

     boost::ptr_vector<Shape> shapes;    
     shapes.push_back(new Point(...));
     shapes.push_back(new Circle(...));
     shapes.front().draw(); // virtual call
    
  3. Используйте объединение C, которое может содержать все возможные элементы, а затем используйте std::vector<UnionType>. Это не очень гибко, так как нам нужно знать все типы элементов заранее (они жестко запрограммированы в объединении), а также хорошо известно, что объединения не взаимодействуют с другими конструкциями C ++ (например, хранимые типы не могут иметь конструкторы).

    struct Point { 
        ...
    };
    struct Circle { 
        ...    
    };
    struct Shape {
        enum Type { PointShape, CircleShape };
        Type type;
        union {
            Point p;
            Circle c;
        } data;
    };
    std::vector<Shape> shapes;
    Point p = { 1, 2 };
    shapes.push_back(p);
    if(shapes.front().type == Shape::PointShape)
        draw_point(shapes.front());
    
  4. Используйте boost::variant, который может содержать все возможные элементы, а затем используйте std::vector<Variant>. Это не так гибко, как объединение, но код для его обработки гораздо более элегантен.

    struct Point { 
        ...
    };
    struct Circle { 
        ...
    };
    typedef boost::variant<Point, Circle> Shape;
    std::vector<Shape> shapes;
    shapes.push_back(Point(1,2));
    draw_visitor(shapes.front());   // use boost::static_visitor
    
  5. Используйте boost::any (который может содержать все, что угодно), а затем std::vector<boost::any>. Это очень гибкий, но интерфейс немного неуклюжий и подвержен ошибкам.

    struct Point { 
        ...
    };
    struct Circle { 
        ...
    };
    typedef boost::any Shape;
    std::vector<Shape> shapes;
    shapes.push_back(Point(1,2));
    if(shapes.front().type() == typeid(Point))    
        draw_point(shapes.front());   
    

Это код полной программы тестирования производительности (по какой-то причине не запускается на кодовой панели). И вот мои результаты производительности:

время с иерархией и повышением :: shared_ptr: 0,491 мкс

время с иерархией и повышением :: ptr_vector: 0,249 микросекунд

время с объединением: 0,043 микросекунды

время с форсированием :: вариант: 0,043 мкс

время с ускорением :: любое: 0,322 микросекунды

Мои выводы:

  • Используйте vector<shared_ptr<Base> > только в том случае, если вам нужна гибкость, обеспечиваемая полиморфизмом во время выполнения , а если , вам необходимо совместное владение. В противном случае у вас будут значительные накладные расходы.

  • Используйте boost::ptr_vector<Base>, если вам нужен полиморфизм времени выполнения, но вам нет дела до совместного владения. Это будет значительно быстрее, чем аналог shared_ptr, и интерфейс будет более дружественным (сохраненные элементы не представлены как указатели).

  • Используйте boost::variant<A, B, C>, если вам не нужна большая гибкость (т. Е. У вас есть небольшой набор типов, которые не будут расти). Он будет светиться быстро, а код будет элегантным.

  • Используйте boost::any, если вам нужна полная гибкость (вы хотите сохранить что-либо).

  • Не используйте союзы. Если вам действительно нужна скорость, то boost::variant такой же быстрый.

Прежде чем я закончу, хочу упомянуть, что вектор std :: unique_ptr будет хорошим вариантом, когда он станет широко доступным (я думаю, он уже в VS2010)

5 голосов
/ 14 февраля 2010

Проблема с vector<vehicle> состоит в том, что объект содержит только транспортные средства. Проблема с vector<vehicle*> заключается в том, что вам нужно выделять и, что более важно, освобождать указатели соответствующим образом.

Это может быть приемлемо, в зависимости от вашего проекта и т. Д. *

Тем не менее, обычно используется какой-то smart-ptr в векторе (vector<boost::shared_ptr<vehicle>> или Qt-что-то, или один из ваших собственных), который обрабатывает освобождение, но все же позволяет хранить объекты разных типов в одном контейнере.

Обновление

Некоторые люди в других ответах / комментариях также упоминали boost::ptr_vector. Это также хорошо работает в качестве контейнера ptr и решает проблему освобождения памяти, владея всеми содержащимися элементами. Я предпочитаю vector<shared_ptr<T>>, так как я могу хранить объекты повсюду и перемещать их, используя входящие и исходящие контейнеры без проблем. Я обнаружил, что это более общая модель использования, которую легче понять мне и другим, и она лучше подходит для более широкого круга проблем.

2 голосов
/ 14 февраля 2010

Проблемы:

  1. Нельзя помещать полиморфные объекты в контейнер, поскольку они могут различаться по размеру - т.е. вы должны использовать указатель.
  2. Из-за того, как работают контейнеры, обычный указатель / автоматический указатель не подходит.

Решение:

  1. Создайте иерархию классов и используйте по крайней мере одну виртуальную функцию в базовом классе (если вы не можете придумать какую-либо функцию для виртуализации, виртуализируйте деструктор - что, как указывал Нил, обычно является обязательным).
  2. Используйте boost :: shared_pointer (это будет в следующем стандарте c ++) - shared_ptr обрабатывает копирование вокруг ad hoc , и контейнеры могут это делать.
  3. Постройте иерархию классов и разместите ваши объекты в куче, т.е. используя new. Указатель на базовый класс должен быть инкапсулирован shared_ptr.
  4. поместите базовый класс shared_pointer в выбранный вами контейнер.

Как только вы поймете "почему" и "как" из приведенных выше пунктов, посмотрите на boost ptr_containers - спасибо Руководству за подсказку.

2 голосов
/ 14 февраля 2010

Скажем, vehicle - это базовый класс, который обладает определенными свойствами, поэтому, наследуя от него, вы скажете car и truck. Тогда вы можете просто сделать что-то вроде:

std::vector<vehicle *> parking_lot;
parking_lot.push_back(new car(x, y));
parking_lot.push_back(new truck(x1, y1));

Это было бы совершенно правильно, а иногда очень полезно. Единственное требование для этого типа обработки объектов - разумная иерархия объектов.

Другими популярными типами объектов, которые можно использовать подобным образом, являются, например, people :) вы видите это почти в каждой книге по программированию.

EDIT:
Конечно, этот вектор может быть упакован с boost::shared_ptr или std::tr1::shared_ptr вместо необработанных указателей для простоты управления памятью. И на самом деле это то, что я бы рекомендовал сделать всеми возможными способами.

EDIT2:
Я удалил не очень соответствующий пример, вот новый:

Допустим, вам необходимо реализовать какую-либо функцию сканирования AV, и у вас есть несколько механизмов сканирования. Таким образом, вы реализуете некоторый класс управления двигателем, скажем, scan_manager, который может вызывать bool scan(...) функцию из них. Затем вы создаете интерфейс двигателя, скажем engine. Он будет иметь virtual bool scan(...) = 0; Затем вы создадите несколько engine s, таких как my_super_engine и my_other_uber_engine, которые оба наследуются от engine и реализуют scan(...). Тогда ваш менеджер движков где-то во время инициализации будет заполнять эти std::vector<engine *> экземплярами my_super_engine и my_other_uber_engine и использовать их, вызывая bool scan(...) для них либо последовательно, либо на основе любых типов сканирования, которые вы хотите выполнить. Очевидно, что эти движки делают в scan(...), остается неизвестным, единственный интересный момент - это bool, поэтому менеджер может использовать их все одинаково без каких-либо изменений.

То же самое можно применить к различным игровым юнитам, таким как scary_enemies, и это будут orks, drunks и другие неприятные существа. Все они реализуют void attack_good_guys(...), и ваш evil_master сделает многие из них и вызовет этот метод.

Это действительно обычная практика, и я вряд ли назвал бы это плохим дизайном, если все эти типы на самом деле связаны.

0 голосов
/ 14 февраля 2010

Вы можете обратиться к ответу Страуструпа на вопрос Почему я не могу назначить вектор на вектор ? .

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