我从假设开始,一般来说,在堆栈中分配小对象,在动态内存中分配大对象是个好主意。另一个假设是,在尝试学习内存、STL 容器和智能指针时,我可能会感到困惑。
考虑以下示例,其中我有一个对象,该对象必须通过智能指针在免费商店中分配,例如,我可以依赖客户端从工厂获取所述对象。此对象包含一些使用 STL 容器专门分配的数据,该容器恰好是 std::vector。在一种情况下,这个数据向量本身是使用一些智能指针动态分配的,而在另一种情况下,我只是不使用智能指针。
如下所述,设计A和设计B之间有什么实际区别吗?
情况 A:
class SomeClass{
public:
SomeClass(){ /* initialize some potentially big STL container */ }
private:
std::vector<double> dataVector_;
};
情况 B:
class SomeOtherClass{
public:
SomeOtherClass() { /* initialize some potentially big STL container,
but is it allocated in any different way? */ }
private:
std::unique_ptr<std::vector<double>> pDataVector_;
};
一些工厂功能。
std::unique_ptr<SomeClass> someClassFactory(){
return std::make_unique<SomeClass>();
}
std::unique_ptr<SomeOtherClass> someOtherClassFactory(){
return std::make_unique<SomeOtherClass>();
}
用例:
int main(){
//in my case I can reliably assume that objects themselves
//are going to always be allocated in dynamic memory
auto pSomeClassObject(someClassFactory());
auto pSomeOtherClassObject(someOtherClassFactory());
return 0;
}
我希望两种设计选择都有相同的结果,但是它们呢? 选择A或B有什么优点或缺点吗?具体来说,我通常应该选择设计 A 是因为它更简单还是有更多的考虑因素?B在道德上是错误的,因为它可以为一个std::vector晃来晃去?
tl;dr : 让一个指向 STL 容器的智能指针是错误的吗?
编辑: 相关答案为像我这样困惑的人指出了有用的附加信息。 使用对象或指向对象的指针作为类成员和内存分配 和作为对象的类成员 - 指针与否?C++ 更改一些谷歌关键字会引导我 分配向量时,它们是使用堆上的内存还是堆栈上的内存?
std::unique_ptr<std::vector<double>>
速度较慢,占用更多内存,唯一的优点是它包含一个额外的可能状态:"向量不存在"。 但是,如果您关心该状态,请改用boost::optional<std::vector>
。 您几乎永远不应该拥有堆分配的容器,绝对不要使用unique_ptr
。 它实际上工作正常,没有"悬空",它只是毫无意义的缓慢。
在这里使用std::unique_ptr
只是浪费,除非你的目标是编译器防火墙(基本上隐藏了编译时对矢量的依赖,但随后你需要一个前向声明到标准容器)。
您正在添加间接寻址,但更重要的是,SomeClass
的全部内容变成了 3 个单独的内存块,以便在访问内容时加载(SomeClass
与指向std::vector's
指向其元素数组的块合并/包含unique_ptr's
块)。此外,您还需要支付一个额外的多余堆开销。
现在,您可以开始想象间接寻址对vector
有帮助的场景,例如,也许您可以在两个SomeClass
实例之间浅表移动/交换unique_ptrs
。是的,但是vector
已经提供了它,上面没有unique_ptr
包装器。它已经具有像empty
这样的状态,您可以将其重用于某些validity/nilness
概念。
请记住,可变大小的容器本身是小对象,而不是大对象,指向潜在的大块。vector
不大,它的动态内容可以。为大型对象添加间接寻址的想法并不是一个糟糕的经验法则,但vector
不是一个大对象。有了移动语义,值得把它想象成一个小内存块,指向一个大块,可以浅层复制和廉价交换。在移动语义之前,有更多的理由将std::vector
之类的东西视为一个不可分割的大对象(尽管它的内容总是可交换的),但现在值得把它想象成一个指向大动态内容的小句柄。
通过类似unique_ptr
引入间接寻址的一些常见原因是:
- 抽象和隐藏。如果您尝试抽象或隐藏某些类型/子类型的具体定义,
Foo
,那么这就是您需要间接寻址的地方,以便那些不知道Foo
是什么的人可以捕获(甚至可能与抽象一起使用)它的句柄。 - 允许一个大的、连续的 1 块类型对象从一个所有者传递到另一个所有者,而不调用副本或使对其或其内容的引用/指针(包括迭代器)无效。
- 一种草率的理由是浪费,但有时在截止日期匆忙中很有用,那就是简单地将
validity/null
状态引入本质上没有它的东西。
有时,作为优化,它很有用,可以提升对象的某些不经常访问的 - 较大成员,以便其经常访问的元素更紧密地适合缓存行中(并且可能与相邻对象)。
unique_ptr
可以让您拆分该对象的内存布局,同时仍符合 RAII。
现在,如果您有一个实际上可以由多个所有者(明智地)拥有的容器,那么将shared_ptr
包装在标准容器之上可能会有更多合法的应用程序。使用unique_ptr
,一次只能有一个所有者拥有该对象,标准容器已经允许您交换和移动彼此的内部内脏(大的动态部分)。因此,我几乎没有理由想到直接用unique_ptr
包装标准容器,因为它已经有点像指向动态数组的智能指针(但具有更多功能来处理动态数据,包括根据需要深度复制它)。
如果我们谈论非标准容器,比如说您正在使用第三方库,该库提供了一些数据结构,其内容可能会变得非常大,但它们无法提供那些廉价的、非无效的move/swap
语义,那么您可能会表面上将其包装在unique_ptr
周围,交换一些创建/访问/销毁开销以恢复那些廉价的move/swap
语义作为解决方法。对于标准容器,不需要此类解决方法。
我同意@MooingDuck;我不认为使用std::unique_ptr
有任何引人注目的优势。但是,如果成员数据非常大并且该类将支持 COW(写入时复制)语义(或数据在多个实例之间共享的任何其他用例),我可以看到std::shared_ptr
用例。