36

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

}

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

4

9 回答 9

26

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

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

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

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

于 2008-09-03T02:19:53.260 回答
12

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

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

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

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

于 2008-09-16T11:08:33.423 回答
10

是的你可以。

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

于 2008-09-04T21:39:09.410 回答
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

话虽如此,我也会先走 shared_ptr 路线!只是觉得这可能有点意思。

于 2008-09-03T02:56:35.607 回答
3

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

  1. 使用 std::optional 或 boost::optional 和访问者模式。此解决方案使添加新类型变得困难,但添加新功能却很容易。
  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;
};

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

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;
}

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

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 在他的演讲中详细讨论了这一点,上面提到的答案中显示了一个实现。

于 2018-04-08T17:46:27.967 回答
2

看看static_castreinterpret_cast
In C++ Programming Language, 3rd ed, Bjarne Stroustrup 在第 130 页描述了它。在第 6 章中有一个完整的部分。
您可以将 Parent 类重新转换为 Child 类。这需要你知道每个是哪个。在书中,Stroustrup 博士谈到了避免这种情况的不同技术。

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

于 2008-09-03T02:29:26.340 回答
2

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

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

你的两个既定目标之间存在基本的张力,所以你应该决定你想要什么,然后尝试设计一些基本上让你得到你想要的东西。可以通过足够聪明的引用计数和足够聪明的工厂实现来获得看起来像值的指针。基本思想是使用引用计数、按需复制和 constness 以及(就因素而言)预处理器、模板和 C++ 的静态初始化规则的组合,以获得关于自动化指针转换的尽可能智能的东西。

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

而且我认为可以做到,但是您必须在 C++ 中提供一个相当封闭的、类似 C# 托管代码的世界(尽管在需要时您可以从中突破到底层 C++)。所以我很同情你的思路。

于 2008-09-03T02:58:41.920 回答
2

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

为了更好地理解这个问题,您可能想看看Scott Mayers 的“More Effective C++” “Item 3: Never Treats arrays polymorphically”。

于 2008-09-03T07:36:41.860 回答
1

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

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

于 2008-09-16T11:24:19.470 回答