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

时间:2008-09-03 02:09:29

标签: c++ stl

作为一般规则,我更喜欢在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

}

我的问题是:我可以拥有蛋糕(价值语义)并且吃它(多态容器)吗?或者我必须使用指针?

9 个答案:

答案 0 :(得分:24)

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

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

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

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

答案 1 :(得分:10)

是的,你可以。

boost.ptr_container库提供标准容器的多态值语义版本。您只需要传入一个指向堆分配对象的指针,容器将获得所有权,所有进一步的操作将提供值语义,但回收所有权除外,它通过使用智能指针为您提供了几乎所有的值语义优势

答案 2 :(得分:10)

我只想指出矢量&lt; Foo&gt;通常比矢量&lt; Foo *&gt;更有效。在向量&lt; Foo&gt;中,所有Foo将在存储器中彼此相邻。假设有一个冷TLB和缓存,第一次读取会将页面添加到TLB并将一大块向量拉入L#缓存;后续读取将使用暖缓存和加载的TLB,偶尔会出现缓存未命中和TLB故障的频率降低。

将此与向量&lt; Foo *&gt;进行对比:填充向量时,从内存分配器获取Foo *。假设您的分配器不是非常智能,(tcmalloc?)或者随着时间的推移慢慢填充向量,每个Foo的位置可能与其他Foo相距很远:可能只有几百个字节,可能相差几兆字节。

在最坏的情况下,当您扫描向量&lt; Foo *&gt;时并解除引用每个指针,你将导致TLB错误和缓存未命中 - 这将比你有一个向量&lt; Foo&gt;的速度慢批次。 (好吧,在最糟糕的情况下,每个Foo都被分页到磁盘,每次读取都会产生磁盘seek()和read()以将页面移回RAM。)

所以,继续使用vector&lt; Foo&gt;适当时。 : - )

答案 3 :(得分:3)

您可能还会考虑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路线!只是觉得这可能有一些兴趣。

答案 4 :(得分:3)

大多数容器类型都想抽象出特定的存储策略,无论是链表,矢量,基于树还是你有什么。出于这个原因,你将无法拥有和消费上述蛋糕(即蛋糕是谎言(注意:有人不得不开这个笑话)。)

那该怎么办?好吧有一些可爱的选项,但大多数会减少到几个主题之一或它们的组合变体:挑选或发明一个合适的智能指针,以一些聪明的方式使用模板或模板模板,使用通用接口的容器它为实现每个容器的双重调度提供了一个钩子。

你的两个既定目标之间存在着基本的紧张关系,所以你应该决定你想要什么,然后尝试设计能够让你基本上达到你想要的东西。它 可以做一些好的和意想不到的技巧,以获得指向看起来像值的指针,具有足够聪明的引用计数和足够聪明的工厂实现。基本思想是使用引用计数和按需复制以及constness和(对于因子)预处理器,模板和C ++的静态初始化规则的组合来获得关于自动化指针转换的尽可能智能的东西。 / p>

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

我认为它可以完成,但你必须在C ++中提供一个相当封闭的,C#管理代码的世界(尽管你可以在需要时从中突破到底层的C ++)。所以我对你的思路有很多同情。

答案 5 :(得分:2)

  

查看 static_cast reinterpret_cast
  在C ++编程语言中,第3版,Bjarne Stroustrup在第130页对此进行了描述。第6章中有相关内容。   您可以将您的Parent类重铸为Child类。这需要你知道每个人哪个是哪个。在书中,Stroustrup博士谈到了避免这种情况的不同技术。

不要这样做。这否定了你首先想要实现的多态性!

答案 6 :(得分:2)

只是为已经说过的所有1800 INFORMATION添加一件事。

你可能想看看Scott Mayers的"More Effective C++"“第3项:永远不要多态处理数组”,以便更好地理解这个问题。

答案 7 :(得分:1)

我正在使用我自己的模板化集合类和暴露值类型语义,但在内部它存储指针。它使用的是自定义迭代器类,当取消引用时获取值引用而不是指针。复制集合会产生深层项目副本,而不是重复的指针,这就是大多数开销所在的地方(一个非常小的问题,考虑我得到的东西)。

这是一个可以满足您需求的想法。

答案 8 :(得分:0)

在搜索此问题的答案时,我遇到了这个和a similar question。在另一个问题的答案中,您将找到两个建议的解决方案:

  1. 使用std :: optional或boost :: optional和访问者模式。此解决方案使添加新类型变得困难,但很容易添加新功能。
  2. 使用类似于Sean Parent presents in his talk的包装类。此解决方案使添加新功能变得困难,但很容易添加新类型。
  3. 包装器定义了类所需的接口,并保存指向一个这样的对象的指针。接口的实现是通过自由函数完成的。

    以下是此模式的示例实现:

    class Shape
    {
    public:
        template<typename T>
        Shape(T t)
            : container(std::make_shared<Model<T>>(std::move(t)))
        {}
    
        friend void draw(const Shape &shape)
        {
            shape.container->drawImpl();
        }
        // add more functions similar to draw() here if you wish
        // remember also to add a wrapper in the Concept and Model below
    
    private:
        struct Concept
        {
            virtual ~Concept() = default;
            virtual void drawImpl() const = 0;
        };
    
        template<typename T>
        struct Model : public Concept
        {
            Model(T x) : m_data(move(x)) { }
            void drawImpl() const override
            {
                draw(m_data);
            }
            T m_data;
        };
    
        std::shared_ptr<const Concept> container;
    };
    

    然后将不同的形状实现为常规结构/类。您可以自由选择是否要使用成员函数或自由函数(但您必须更新上述实现以使用成员函数)。我更喜欢免费功能:

    struct Circle
    {
        const double radius = 4.0;
    };
    
    struct Rectangle
    {
        const double width = 2.0;
        const double height = 3.0;
    };
    
    void draw(const Circle &circle)
    {
        cout << "Drew circle with radius " << circle.radius << endl;
    }
    
    void draw(const Rectangle &rectangle)
    {
        cout << "Drew rectangle with width " << rectangle.width << endl;
    }
    

    您现在可以将CircleRectangle个对象添加到同一个std::vector<Shape>

    int main() {
        std::vector<Shape> shapes;
        shapes.emplace_back(Circle());
        shapes.emplace_back(Rectangle());
        for (const auto &shape : shapes) {
            draw(shape);
        }
        return 0;
    }
    

    这种模式的缺点是它需要在界面中有大量的样板,因为每个函数需要定义三次。 好处是你得到了复制语义:

    int main() {
        Shape a = Circle();
        Shape b = Rectangle();
        b = a;
        draw(a);
        draw(b);
        return 0;
    }
    

    这会产生:

    Drew rectangle with width 2
    Drew rectangle with width 2
    

    如果您对shared_ptr感到担心,可以将其替换为unique_ptr。 但是,它将不再是可复制的,您必须移动所有对象或手动执行复制。 Sean Parent在他的演讲中对此进行了详细讨论,并在上述答案中显示了实现。