题
作为一般规则,我更喜欢在 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 使用引用计数,因此在删除所有引用之前它不会删除底层实例。
其他提示
是的你可以。
boost.ptr_container 库提供标准容器的多态值语义版本。您只需传入一个指向堆分配对象的指针,容器将获得所有权,并且所有进一步的操作都将提供值语义,除了回收所有权,这通过使用智能指针为您提供了几乎所有值语义的好处。
我只是想指出向量<Foo>通常比向量<Foo*>更有效。在向量 <Foo> 中,所有 Foo 在内存中都将彼此相邻。假设 TLB 和缓存是冷的,第一次读取会将页面添加到 TLB 并将矢量块拉入 L# 缓存;后续读取将使用热缓存和已加载的 TLB,偶尔会出现缓存未命中和不太频繁的 TLB 故障。
将此与向量 <Foo*> 进行对比:当您填充向量时,您可以从内存分配器中获取 Foo*。假设您的分配器不是非常智能(tcmalloc?)或者您随着时间的推移慢慢填充向量,每个 Foo 的位置可能与其他 Foo 相距很远:也许只是相隔数百字节,也许相隔兆字节。
在最坏的情况下,当您扫描向量 <Foo*> 并取消引用每个指针时,您将导致 TLB 错误和缓存未命中 - 这最终将成为 很多 比使用矢量 <Foo> 慢。(好吧,在最坏的情况下,每个 Foo 都已被分页到磁盘,并且每次读取都会引发磁盘eek()和read()以将页面移回RAM。)
因此,在适当的时候继续使用 vector<Foo> 。:-)
大多数容器类型都希望抽象出特定的存储策略,无论是链表、向量、基于树还是其他的。因此,你在拥有和食用上述蛋糕时都会遇到麻烦(即,蛋糕是谎言(注意:必须有人开这个玩笑))。
那么该怎么办?嗯,有一些可爱的选择,但大多数都会减少为几个主题之一或它们组合的变体:选择或发明一个合适的智能指针,以某种巧妙的方式使用模板或模板模板,使用容器的通用接口,该接口提供了用于实现每个容器双重调度的钩子。
你的两个既定目标之间存在基本的张力,因此你应该决定你想要什么,然后尝试设计一些基本上可以满足你想要的东西。它 是 可以做一些漂亮且意想不到的技巧,让指针看起来像具有足够聪明的引用计数和足够聪明的工厂实现的值。基本思想是使用引用计数、按需复制和常量以及(对于因子)预处理器、模板和 C++ 静态初始化规则的组合,以获得关于自动指针转换尽可能智能的东西。
过去,我花了一些时间尝试设想如何使用虚拟代理/信封字母/带有引用计数指针的可爱技巧来完成类似 C++ 中值语义编程的基础之类的事情。
我认为这是可以做到的,但您必须在 C++ 中提供一个相当封闭的、类似 C# 托管代码的世界(尽管您可以在需要时突破到底层 C++)。所以我非常同情你的想法。
您也可以考虑 提升::任何. 。我已将其用于异构容器。当读回该值时,您需要执行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路线!只是觉得这可能会引起一些兴趣。
看一眼 静态类型转换 和 重新解释_cast
Bjarne Stroustrup 在《C++ 编程语言》第 3 版第 130 页上对此进行了描述。第 6 章中有一个完整的章节对此进行了讨论。
您可以将您的父类重新转换为子类。这需要你知道每一个是什么时候的。在书中,博士。Stroustrup 讨论了避免这种情况的不同技术。
不要这样做。这否定了您首先想要实现的多态性!
只是为了向所有内容添加一件事 1800条信息 已经说过了。
您可能想看一下 “更有效的 C++” 作者:斯科特·梅耶斯“第 3 项:永远不要以多态方式对待数组”,以便更好地理解这个问题。
我使用自己的模板化集合类,具有公开的值类型语义,但它在内部存储指针。它使用自定义迭代器类,当取消引用时,它会获取值引用而不是指针。复制集合会产生深层项目副本,而不是重复的指针,这就是最大开销所在的地方(一个非常小的问题,考虑到我得到的是什么)。
这是一个可以满足您需求的想法。
在寻找这个问题的答案时,我遇到了这个和 类似的问题. 。在另一个问题的答案中,您会发现两个建议的解决方案:
- 使用std::可选或boost::可选和访问者模式。这种解决方案使得添加新类型变得困难,但添加新功能却很容易。
- 使用类似于以下的包装类 肖恩·帕伦特 (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;
}
您现在可以添加两者 Circle
和 Rectangle
对象相同 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 在他的演讲中详细讨论了这一点,上面提到的答案中显示了一个实现。