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

And*_*ewR 32 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

}
Run Code Online (Sandbox Code Playgroud)

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

180*_*ION 24

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

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

vector<shared_ptr<Parent>> vec;
vec.push_back(shared_ptr<Parent>(new Child()));
Run Code Online (Sandbox Code Playgroud)

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

  • 我从未说过我的解决方案解决了价值语义问题.我说这是"合理的解决方案".如果您知道一种具有多态值语义的方法,那么请立即采取行动并收集您的诺贝尔奖. (5认同)
  • 这个答案没有解决价值语义问题.shared_ptr <T>提供对从T派生的类的引用语义,对于instace,shared_ptr <Base> a,b; b.reset(new Derived1); a = b; 不会复制Derived1对象. (4认同)
  • **注意`shared_ptr`现在在`std`命名空间中,从C++ 11开始.**你想[使用这个](http://en.cppreference.com/w/cpp/memory/ shared_ptr)而不是Boost的. (4认同)
  • `boost :: ptr_vector`通常比`std :: vector <boost :: shared_ptr <T >>更便宜,更简单. (3认同)
  • 有许多`clone_ptr`或`value_ptr`类型可以实现这一目的. (2认同)

ben*_*ben 10

是的你可以.

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


012*_*816 10

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

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

在最坏的情况下,当你通过矢量扫描<美孚*>和非关联每个指针,你将收取一定的TLB错误和缓存未命中-这将最终会被很多比,如果你有一个vector <美孚>慢.(好吧,在最糟糕的情况下,每个Foo都被分页到磁盘,每次读取都会产生磁盘seek()和read()以将页面移回RAM.)

所以,在适当的时候继续使用vector <Foo>.:-)

  • +1用于缓存考虑!这个问题将来会变得更加相关. (3认同)
  • 它取决于Foo :: Foo(const Foo&)的价格,因为值语义容器需要在插入时调用它. (2认同)

Ada*_*dge 5

您也可以考虑boost::any。我已经将它用于异构容器。读回值时,您需要执行 any_cast。如果失败,它将抛出 bad_any_cast。如果发生这种情况,您可以抓住并转到下一个类型。

相信如果您尝试将派生类 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
Run Code Online (Sandbox Code Playgroud)

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


dra*_*gly 5

在寻找这个问题的答案时,我遇到了这个问题和一个类似的问题。在另一个问题的答案中,您会发现两个建议的解决方案:

  1. 使用std::可选或boost::可选和访问者模式。这种解决方案使得添加新类型变得困难,但添加新功能却很容易。
  2. 使用类似于Sean Parent 在他的演讲中介绍的包装类。这种解决方案使得添加新功能变得困难,但添加新类型却很容易。

包装器定义类所需的接口,并保存指向此类对象的指针。接口的实现是通过自由函数完成的。

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

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;
};
Run Code Online (Sandbox Code Playgroud)

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

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;
}
Run Code Online (Sandbox Code Playgroud)

您现在可以将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;
}
Run Code Online (Sandbox Code Playgroud)

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

int main() {
    Shape a = Circle();
    Shape b = Rectangle();
    b = a;
    draw(a);
    draw(b);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

这会产生:

Drew rectangle with width 2
Drew rectangle with width 2
Run Code Online (Sandbox Code Playgroud)

如果您担心shared_ptr,可以将其替换为unique_ptr。但是,它将不再可复制,您必须移动所有对象或手动实施复制。Sean Parent 在他的演讲中详细讨论了这一点,上面提到的答案中显示了一个实现。