我可以在C ++中使用具有值语义的多态容器吗?

时间:2020-03-05 18:46:51  来源:igfitidea点击:

通常,我更喜欢在C ++中使用值而不是指针语义(即,使用" vector <Class>"而不是" vector <Class *>")。通常,由于不必记住删除动态分配的对象,因此可以弥补轻微的性能损失。

不幸的是,当我们要存储所有都源自同一基础的各种对象类型时,值集合不起作用。请参见下面的示例。

#include <iostream>

using namespace std;

class Parent
{
    public:
        Parent() : parent_mem(1) {}
        virtual void write() { cout << "Parent: " << parent_mem << endl; }
        int parent_mem;
};

class Child : public Parent
{
    public:
        Child() : child_mem(2) { parent_mem = 2; }
        void write() { cout << "Child: " << parent_mem << ", " << child_mem << endl; }

        int child_mem;
};

int main(int, char**)
{
    // I can have a polymorphic container with pointer semantics
    vector<Parent*> pointerVec;

    pointerVec.push_back(new Parent());
    pointerVec.push_back(new Child());

    pointerVec[0]->write(); 
    pointerVec[1]->write(); 

    // Output:
    //
    // Parent: 1
    // Child: 2, 2

    // But I can't do it with value semantics

    vector<Parent> valueVec;

    valueVec.push_back(Parent());
    valueVec.push_back(Child());    // gets turned into a Parent object :(

    valueVec[0].write();    
    valueVec[1].write();    

    // Output:
    // 
    // Parent: 1
    // Parent: 2

}

我的问题是:我可以吃蛋糕(值语义),也可以吃它(多态容器)吗?还是我必须使用指针?

解决方案

回答

由于不同类的对象将具有不同的大小,因此如果将它们存储为值,最终会遇到切片问题。

一种合理的解决方案是存储容器安全的智能指针。我通常使用boost :: shared_ptr,它可以安全地存储在容器中。请注意,std :: auto_ptr不是。

vector<shared_ptr<Parent>> vec;
vec.push_back(shared_ptr<Parent>(new Child()));

shared_ptr使用引用计数,因此在删除所有引用之前,它不会删除基础实例。

回答

Take a look at static_cast and reinterpret_cast

  In C++ Programming Language, 3rd ed, Bjarne Stroustrup describes it on page 130. There's a whole section on this in Chapter 6.

  You can recast your Parent class to Child class. This requires you to know when each one is which. In the book, Dr. Stroustrup talks about different techniques to avoid this situation.

不要这样做。首先,这会否定我们要实现的多态性!

回答

我们可能还会考虑boost :: any。我已经将其用于异构容器。当读回值时,我们需要执行any_cast。如果失败,它将抛出bad_any_cast。如果发生这种情况,我们可以赶上下一个类型。

我相信,如果我们尝试将任何派生类广播到它的基类,它将抛出bad_any_cast。我尝试过这个:

// But you sort of can do it with boost::any.

  vector<any> valueVec;

  valueVec.push_back(any(Parent()));
  valueVec.push_back(any(Child()));        // remains a Child, wrapped in an Any.

  Parent p = any_cast<Parent>(valueVec[0]);
  Child c = any_cast<Child>(valueVec[1]);
  p.write();
  c.write();

  // Output:
  //
  // Parent: 1
  // Child: 2, 2

  // Now try casting the child as a parent.
  try {
      Parent p2 = any_cast<Parent>(valueVec[1]);
      p2.write();
  }
  catch (const boost::bad_any_cast &e)
  {
      cout << e.what() << endl;
  }

  // Output:
  // boost::bad_any_cast: failed conversion using boost::any_cast

话虽如此,我也将先走shared_ptr路线!只是认为这可能会引起一些兴趣。

回答

大多数容器类型都希望抽象出特定的存储策略,无论是链表,矢量,基于树还是我们拥有的存储策略。因此,拥有和食用上述蛋糕都将遇到麻烦(即蛋糕是骗人的(注意:有人不得不开这个玩笑))。

那么该怎么办?好了,有一些可爱的选项,但是大多数选项都会简化为以下几个主题之一或者它们的组合:选择或者发明合适的智能指针,以某种巧妙的方式使用模板或者模板模板,使用包含对象的通用界面它为实现每个容器的双重调度提供了一个钩子。

这两个既定目标之间存在基本的紧张关系,因此我们应该确定所需的内容,然后尝试设计一些基本可以使我们获得所需内容的东西。可以做一些不错的和意想不到的技巧,以使指针看起来像具有足够聪明的引用计数和足够聪明的工厂实现的值。基本思想是使用引用计数,按需复制和constness,以及(针对该因素)预处理器,模板和C ++的静态初始化规则的组合,以获取关于自动化指针转换的尽可能智能的信息。

过去,我花了一些时间来尝试设想如何使用虚拟代理/信封信/具有引用计数指针的可爱技巧来完成类似C ++中的值语义编程的基础。

而且我认为可以做到,但是我们必须在C ++中提供一个相当封闭的,类似于C#管理的代码的世界(尽管我们可以在需要时从中进入基础C ++)。因此,我很同情思路。

回答

只是为了在已经说过的1800条信息中添加一件事。

我们可能想看看Scott Mayers的"更有效的C ++","项目3:从不对数组进行多态处理",以便更好地理解此问题。

回答

是的你可以。

boost.ptr_container库提供标准容器的多态值语义版本。我们只需传递指向堆分配对象的指针,容器将获得所有权,并且所有其他操作将提供值语义,除了收回所有权之外,这通过使用智能指针为我们提供了值语义的几乎所有好处。 。

回答

我只想指出,vector <Foo>通常比vector <Foo *>更有效。在vector <Foo>中,所有的Foos在内存中将彼此相邻。假设TLB和缓存处于冷状态,则第一次读取会将页面添加到TLB,并将向量的一部分拉入Lcache;随后的读取将使用热缓存和已加载的TLB,偶尔会出现缓存未命中和不太频繁的TLB故障。

将其与vector <Foo *>进行比较:填充矢量时,将从内存分配器中获取Foo *。假设分配器不是非常聪明(tcmalloc?),或者随着时间的推移缓慢地填充向量,则每个Foo的位置可能与另一个Foos相距很远:也许只是数百个字节,也许是兆字节。

在最坏的情况下,当我们扫描vector <Foo *>并取消对每个指针的引用时,将导致TLB错误和缓存未命中-这最终将比使用vector <Foo>时要慢得多。 (好吧,在最坏的情况下,每个Foo都已被调出到磁盘上,并且每次读取都会产生磁盘seek()和read()来将页面移回到RAM中。)

因此,请在适当的时候继续使用vector <Foo>。 :-)

回答

我正在使用自己的具有公开值类型语义的模板化集合类,但在内部它存储指针。它使用了一个自定义的迭代器类,当取消引用时,该类将获得值引用而不是指针。复制集合将进行深层项目复制,而不是重复的指针复制,这就是大部分开销所在的地方(这是一个很小的问题,考虑到我所得到的)。

这个想法可以满足需求。