将层次结构中的不同类放在 C++ 的一个容器中

发布于 2024-08-22 01:26:47 字数 307 浏览 3 评论 0 原文

有时我们必须将不同的对象放在一个容器的同一层次结构中。我读过一些文章说有一些技巧和陷阱。然而,我对这个问题没有大局观。事实上,这种情况在现实世界中经常发生。

例如,一个停车场必须容纳不同类型的汽车;动物园必须容纳不同类型的动物;书店必须包含不同类型的书籍。

我记得有一篇文章说以下两种都不是好的设计,但我忘了它在哪里。

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

有人可以为此类问题提供一些基本规则吗?

Some times we have to put different objects in the same hierarchy in one container. I read some article saying there are some tricks and traps. However, I have no big picture about this question. Actually, this happens a lot in the real word.

For example, a parking lot has to contain different types of cars; a zoo has to contain different types of animals; a book store has to contain different types of books.

I remember that one article saying neither of the following is a good design, but I forgot where it is.

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

Can anybody offer some basic rules for this kind of question?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

匿名。 2024-08-29 01:26:47

我在回复类似问题时学到了很多同一个作者,所以我忍不住在这里做同样的事情。简而言之,我编写了一个基准来比较以下在标准容器中存储异构元素问题的方法:

  1. 为每种类型的元素创建一个类,并让它们全部继承自公共基类并存储多态基类std::vector 中的指针>。这可能是更通用和灵活的解决方案:

    结构形状{
        ...
    };
    结构点:公共形状{ 
        ...
    };
    结构圆:公共形状{ 
        ...
    };
    std::vector >形状;    
    shape.push_back(new Point(...));
    shape.push_back(new Circle(...));
    shape.front()->draw(); // 虚拟调用
    
  2. 与 (1) 相同,但将多态指针存储在 boost::ptr_vector 中。这不太通用,因为元素完全由向量拥有,但在大多数情况下应该足够了。 boost::ptr_vector 的优点之一是它具有 std::vector 的接口(不带 *),因此使用起来更简单。 p>

     boost::ptr_vector<形状>;形状;    
     shape.push_back(new Point(...));
     shape.push_back(new Circle(...));
     shape.front().draw(); // 虚拟调用
    
  3. 使用可包含所有可能元素的 C 联合,然后使用 std::vector。这不是很灵活,因为我们需要提前知道所有元素类型(它们被硬编码到联合中),而且众所周知,联合不能与其他 C++ 结构很好地交互(例如,存储的类型不能具有构造函数)。

    结构点{ 
        ...
    };
    结构圆{ 
        ...    
    };
    结构形状{
        枚举类型 { PointShape, CircleShape };
        类型类型;
        联盟{
            点p;
            圈c;
        } 数据;
    };
    std::vector<形状>形状;
    点 p = { 1, 2 };
    形状.push_back(p);
    if(shapes.front().type == Shape::PointShape)
        绘制点(shapes.front());
    
  4. 使用可包含所有可能元素的 boost::variant,然后使用 std::vector。这不像联合那样灵活,但处理它的代码要优雅得多。

    结构点{ 
        ...
    };
    结构圆{ 
        ...
    };
    typedef boost::variant<点、圆>形状;
    std::vector<形状>形状;
    shape.push_back(Point(1,2));
    draw_visitor(shapes.front()); // 使用 boost::static_visitor
    
  5. 使用 boost::any (可以包含任何内容),然后使用 std::vector。这非常灵活,但界面有点笨拙且容易出错。

    结构点{ 
        ...
    };
    结构圆{ 
        ...
    };
    typedef boost::任何形状;
    std::vector<形状>形状;
    shape.push_back(Point(1,2));
    if(shapes.front().type() == typeid(Point))    
        绘制点(shapes.front());   
    

这是完整基准测试程序的代码(由于某种原因无法在键盘上运行)。这是我的表现结果:

层次结构和 boost::shared_ptr 的时间:0.491 微秒

层次结构和 boost::ptr_vector 的时间:0.249 微秒

联合时间:0.043 微秒

boost::variant 的时间:0.043 微秒

boost::any 的时间:0.322 微秒

我的结论:

  • < p>使用vector; >仅当您需要运行时多态性提供的灵活性并且您需要共享所有权时。否则,您将产生大量开销。

  • 如果您需要运行时多态性但不关心共享所有权,请使用boost::ptr_vector。它将比对应的 shared_ptr 快得多,并且界面将更加友好(存储的元素不会像指针一样呈现)。

  • 如果您不需要太多灵活性(即您有一小组不会增长的类型),请使用boost::variant。它会很快并且代码会很优雅。

  • 如果您需要完全的灵活性(您想要存储任何内容),请使用 boost::any

  • 不要使用联合体。如果您确实需要速度,那么 boost::variant 也同样快。

在我结束之前,我想提一下 std::unique_ptr 的向量将是一个不错的选择变得广泛可用(我认为它已经在 VS2010 中)

I learnt a lot writing my reply to a similar question by the same author, so I couldn't resist to do the same here. In short, I've written a benchmark to compare the following approaches to the problem of storing heterogeneous elements in a standard container:

  1. Make a class for each type of element and have them all inherit from a common base and store polymorphic base pointers in a std::vector<boost::shared_ptr<Base> >. This is probably the more general and flexible solution:

    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. Same as (1) but store the polymorphic pointers in a boost::ptr_vector<Base>. This is a bit less general because the elements are owned exclusively by the vector, but it should suffice most of the times. One advantage of boost::ptr_vector is that it has the interface of a std::vector<Base> (without the *), so its simpler to use.

     boost::ptr_vector<Shape> shapes;    
     shapes.push_back(new Point(...));
     shapes.push_back(new Circle(...));
     shapes.front().draw(); // virtual call
    
  3. Use a C union that can contain all possible elements and then use a std::vector<UnionType>. This is not very flexible as we need to know all element types in advance (they are hard-coded into the union) and also unions are well known for not interacting nicely with other C++ constructs (for example, the stored types can't have constructors).

    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. Use a boost::variant that can contain all possible elements and then use a std::vector<Variant>. This is not very flexible like the union but the code to deal with it is much more elegant.

    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. Use boost::any (which can contain anything) and then a std::vector<boost::any>. That is very flexible but the interface is a little clumsy and error prone.

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

This is the code of the full benchmark program (doesn't run on codepad for some reason). And here are my performance results:

time with hierarchy and boost::shared_ptr: 0.491 microseconds

time with hierarchy and boost::ptr_vector: 0.249 microseconds

time with union: 0.043 microseconds

time with boost::variant: 0.043 microseconds

time with boost::any: 0.322 microseconds

My conclusions:

  • Use vector<shared_ptr<Base> > only if you need the flexibility provided by runtime polymorphism and if you need shared ownership. Otherwise you'll have significant overhead.

  • Use boost::ptr_vector<Base> if you need runtime polymorphism but don't care about shared ownership. It will be significantly faster than the shared_ptr counterpart and the interface will be more friendly (stored elements not presented like pointers).

  • Use boost::variant<A, B, C> if you don't need much flexibility (i.e. you have a small set of types which will not grow). It will be lighting fast and the code will be elegant.

  • Use boost::any if you need total flexibility (you want to store anything).

  • Don't use unions. If you really need speed then boost::variant is as fast.

Before I finish I want to mention that a vector of std::unique_ptr will be a good option when it becomes widely available (I think it's already in VS2010)

零度℉ 2024-08-29 01:26:47

vector 的问题是该对象仅包含车辆。 vector 的问题是您需要适当地分配指针,更重要的是,释放指针。

这可能是可以接受的,具体取决于您的项目等...

但是,人们通常在向量中使用某种 smart-ptr (vector> 或 Qt-处理释放的东西,或者你自己的东西),但仍然允许在同一个容器中存储不同类型的对象。

更新

有些人在其他答案/评论中也提到了 boost::ptr_vector。这也可以很好地用作 ptr 的容器,并通过拥有所有包含的元素来解决内存释放问题。我更喜欢 vector> 因为这样我就可以将对象存储在各处,并使用容器移入和移出它们,而不会出现任何问题。我发现这是一种更通用的使用模型,对我和其他人来说更容易掌握,并且可以更好地应用于更大的问题集。

The problem with vector<vehicle> is that the object only holds vehicles. The problem with vector<vehicle*> is that you need to allocate and, more importantly, free the pointers appropriately.

This might be acceptable, depending on your project, etc...

However, one usually uses some kind of smart-ptr in the vector (vector<boost::shared_ptr<vehicle>> or Qt-something, or one of your own) that handles deallocation, but still permits storing different types objects in the same container.

Update

Some people have, in other answers/comments, also mentioned boost::ptr_vector. That works well as a container-of-ptr's too, and solves the memory deallocation problem by owning all the contained elements. I prefer vector<shared_ptr<T>> as I can then store objects all over the place, and move them using in and out of containers w/o issues. It's a more generic usage model that I've found is easier for me and others to grasp, and applies better to a larger set of problems.

熊抱啵儿 2024-08-29 01:26:47

问题是:

  1. 您不能将多态对象放入容器中,因为它们的大小可能不同 - 即,您必须使用指针。
  2. 由于容器的工作方式,普通指针/自动指针不适合。

解决方案是:

  1. 创建一个类层次结构,并在基类中至少使用一个虚拟函数(如果您想不出任何要虚拟化的函数,请虚拟化析构函数 - 正如尼尔指出的那样,这通常是必须的)。
  2. 使用 boost::shared_pointer (这将是在下一个 C++ 标准中)——shared_ptr 处理临时复制,并且容器可以执行此操作。
  3. 构建类层次结构并在堆上分配对象——即通过使用 new。指向基类的指针必须由shared_ptr封装。
  4. 将基类shared_pointer放入您选择的容器中。

一旦您了解了上述几点的“原因和方法”,请查看 boost ptr_containers——感谢手册的提示。

The problems are:

  1. You cannot place polymorphic objects into a container since they may differ in size --i.e., you must use a pointer.
  2. Because of how containers work a normal pointer / auto pointer is not suitable.

The solution is :

  1. Create a class hierarchy, and use at least one virtual function in your base class (if you can't think of any function to virtualize, virtualize the destructor -- which as Neil pointed out is generally a must).
  2. Use a boost::shared_pointer (this will be in the next c++ standard) -- shared_ptr handles being copied around ad hoc, and containers may do this.
  3. Build a class hierarchy and allocate your objects on the heap --i.e., by using new. The pointer to the base class must be encapsulated by a shared_ptr.
  4. place the base class shared_pointer into your container of choice.

Once you understand the "whys and hows" of the points above look at the boost ptr_containers -- thanks to Manual for the tip.

萌面超妹 2024-08-29 01:26:47

假设vehicle是一个基类,它具有某些属性,然后,从它继承,你可以说有cartruck。然后你可以这样做:

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

这将是完全有效的,事实上有时非常有用。这种类型的对象处理的唯一要求是对象的合理层次结构。

其他可以像这样使用的流行对象类型是例如 people :) 您几乎在每本编程书中都能看到这一点。

编辑:
当然,该向量可以用 boost::shared_ptrstd::tr1::shared_ptr 来包装,而不是原始指针,以便于内存管理。事实上,这是我建议尽一切可能去做的事情。

编辑2:
我删除了一个不太相关的示例,这是一个新示例:

假设您要实现某种 AV 扫描功能,并且您有多个扫描引擎。因此,您实现某种引擎管理类,例如 scan_manager ,它可以调用这些引擎的 bool scan(...) 函数。然后创建一个引擎接口,例如engine。它将有一个virtual bool scan(...) = 0;然后你制作一些引擎,例如my_super_enginemy_other_uber_engine ,两者都继承自engine并实现scan(...)。然后,您的引擎管理器会在初始化期间的某个地方用 my_super_enginemy_other_uber_engine 的实例填充 std::vector 并通过以下方式使用它们按顺序或根据您想要执行的任何类型的扫描对它们调用 bool scan(...) 。显然,这些引擎在 scan(...) 中执行的操作仍然未知,唯一有趣的一点是 bool,因此管理器可以以相同的方式使用它们,而无需任何操作修改。

同样的情况也适用于各种游戏单位,例如 scary_enemies 以及 orksdrunks 和其他令人不快的生物。它们都实现了 void Attack_good_guys(...) 并且您的 evil_master 会创建其中的许多并调用该方法。

这确实是一种常见的做法,只要所有这些类型实际上都是相关的,我就不会称之为糟糕的设计。

Say vehicle is a base class, that has certain properties, then, inheriting from it you have say a car, and a truck. Then you can just do something like:

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

This would be perfectly valid, and in fact very useful sometimes. The only requirement for this type of object handling is sane hierarchy of objects.

Other popular type of objects that can be used like that are e.g. people :) you see that in almost every programming book.

EDIT:
Of course that vector can be packed with boost::shared_ptr or std::tr1::shared_ptr instead of raw pointers for ease of memory management. And in fact that's something I would recommend to do by all means possible.

EDIT2:
I removed a not very relevant example, here's a new one:

Say you are to implement some kind of AV scanning functionality, and you have multiple scanning engines. So you implement some kind of engine management class, say scan_manager which can call bool scan(...) function of those. Then you make an engine interface, say engine. It would have a virtual bool scan(...) = 0; Then you make a few engines like my_super_engine and my_other_uber_engine, which both inherit from engine and implement scan(...). Then your engine manager would somewhere during initialization fill that std::vector<engine *> with instances of my_super_engine and my_other_uber_engine and use them by calling bool scan(...) on them either sequentially, or based on whatever types of scanning you'd like to perform. Obviously what those engines do in scan(...) remains unknown, the only interesting bit is that bool, so the manager can use them all in the same way without any modification.

Same can be applied to various game units, like scary_enemies and those would be orks, drunks and other unpleasant creatures. They all implement void attack_good_guys(...) and your evil_master would make many of them and call that method.

This is indeed a common practice, and I would hardly call it bad design for as long as all those types actually are related.

小嗲 2024-08-29 01:26:47

你可以参考这个Stroustrup对问题的回答为什么我不能分配一个向量<苹果*>到一个向量<水果*>?

You can refer to this Stroustrup's answer to the question Why can't I assign a vector< Apple*> to a vector< Fruit*>?.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文