对于数据成员,如果包含对象已在动态内存中,则动态分配此变量(或不动态分配)之间是否有任何区别



我从假设开始,一般来说,在堆栈中分配小对象,在动态内存中分配大对象是个好主意。另一个假设是,在尝试学习内存、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引入间接寻址的一些常见原因是:

  1. 抽象和隐藏。如果您尝试抽象或隐藏某些类型/子类型的具体定义,Foo,那么这就是您需要间接寻址的地方,以便那些不知道Foo是什么的人可以捕获(甚至可能与抽象一起使用)它的句柄。
  2. 允许一个大的、连续的 1 块类型对象从一个所有者传递到另一个所有者,而不调用副本或使对其或其内容的引用/指针(包括迭代器)无效。
  3. 一种草率的理由是浪费,但有时在截止日期匆忙中很有用,那就是简单地将validity/null状态引入本质上没有它的东西。
  4. 有时,作为优化,它很有用,可以提升对象的某些不经常访问的
  5. 较大成员,以便其经常访问的元素更紧密地适合缓存行中(并且可能与相邻对象)。unique_ptr可以让您拆分该对象的内存布局,同时仍符合 RAII。

现在,如果您有一个实际上可以由多个所有者(明智地)拥有的容器,那么将shared_ptr包装在标准容器之上可能会有更多合法的应用程序。使用unique_ptr,一次只能有一个所有者拥有该对象,标准容器已经允许您交换和移动彼此的内部内脏(大的动态部分)。因此,我几乎没有理由想到直接用unique_ptr包装标准容器,因为它已经有点像指向动态数组的智能指针(但具有更多功能来处理动态数据,包括根据需要深度复制它)。

如果我们谈论非标准容器,比如说您正在使用第三方库,该库提供了一些数据结构,其内容可能会变得非常大,但它们无法提供那些廉价的、非无效的move/swap语义,那么您可能会表面上将其包装在unique_ptr周围,交换一些创建/访问/销毁开销以恢复那些廉价的move/swap语义作为解决方法。对于标准容器,不需要此类解决方法。

我同意@MooingDuck;我不认为使用std::unique_ptr有任何引人注目的优势。但是,如果成员数据非常大并且该类将支持 COW(写入时复制)语义(或数据在多个实例之间共享的任何其他用例),我可以看到std::shared_ptr用例。

最新更新