在类中存储指针或对象?

时间:2011-09-13 17:02:49

标签: c++ class object pointers move-semantics

只是一个设计/优化问题。什么时候存储指针或对象?为什么?例如,我相信这两个工作(除了编译错误):

class A{
  std::unique_ptr<Object> object_ptr;
};

A::A():object_ptr(new Object()){}

class B{
  Object object;
};

B::B():object(Object()){}

我相信在堆栈或堆上实例化时会出现一个区别吗?

例如:

   int main(){
      std::unique_ptr<A> a_ptr;
      std::unique_ptr<B> b_ptr;
      a_ptr = new A(); //(*object_ptr) on heap, (*a_ptr) on heap?
      b_ptr = new B(); //(*object_ptr) on heap, object on heap?

      A a;   //a on stack, (*object_ptr) on heap?
      B b;   //b on stack, object on stack?
}

此外,sizeof(A)应该是&lt; sizeof(B)? 我还有其他问题吗? (丹尼尔在评论中提醒我关于他的相关帖子中的继承问题)

因此,由于堆栈分配通常比堆分配快,但A的大小比B小,这些权衡中的哪一个如果没有在有问题的情况下测试性能,即使使用移动语义也无法回答?或者一些拇指规则当使用一个比另一个更有利时? (San Jacinto纠正了我关于堆栈/堆分配更快,而不是堆栈/堆)

我猜想更多的副本构建会导致相同的性能问题,(3个副本将比启动第一个实例的性能大约3倍)。但是移动构造可能更有利于尽可能多地使用堆栈???

这是一个相关问题,但不完全相同。 C++ STL: should I store entire objects, or pointers to objects?

谢谢!

4 个答案:

答案 0 :(得分:6)

如果你的A class中有一个大对象,那么我会存储一个指向它的指针,但是对于小对象或原始类型,你不应该真的需要存储指针,在大多数情况下。

此外,当某些东西存储在堆栈上或堆上时(freestore)实际上是依赖于实现的,并且A a并不总是被保证在堆栈中。

最好将其称为自动对象,因为它的存储持续时间由其声明的函数范围决定。当函数返回时,a将被销毁。

指针需要使用new并且它确实带来了一些开销,但是在今天的机器上,我会说在大多数情况下这是微不足道的,除非你开始new数百万对象,然后你将开始看到性能问题。

每种情况都不同,当你应该而且不应该使用指针而不是自动对象时,你的情况基本上是依赖

答案 1 :(得分:5)

堆不比堆栈“慢”。堆分配可能比堆栈分配慢,如果您以不存在大量连续内存访问的方式设计对象和数据结构,则较差的缓存局部性可能会导致大量缓存未命中。因此,从这个角度来看,这取决于您的设计和代码使用目标是什么。

即使把它放在一边,你也必须质疑你的复制语义。如果你想要对象的深层副本(并且你的对象的对象也被深深复制),那么为什么甚至存储指针呢?如果由于复制语义而有共享内存是可以的,那么存储指针但确保你没有在dtor中释放两次内存。

我倾向于在两种情况下使用指针:类成员初始化顺序很重要,我正在将依赖注入到对象中。在大多数其他情况下,我使用非指针类型。

编辑:当我使用指针时还有两种情况:1)避免循环包含依赖项(虽然我可能在某些情况下使用引用),2)意图使用多态函数调用。

答案 2 :(得分:5)

这取决于很多具体因素,任何一种方法都有其优点。我会说如果你通过动态分配专门使用外部对象,那么你也可以让所有成员直接成员并避免额外的成员分配。另一方面,如果自动分配外部对象,则可能应通过unique_ptr处理大型成员。

仅通过指针处理成员还有一个好处:删除编译时依赖项,外部类的头文件可以通过内部类的前向声明来逃避,而不是要求完全包含内部类的标题(“PIMPL”)。在大型项目中,这种脱钩可能在经济上是合理的。

答案 3 :(得分:2)

在某些情况下,您几乎别无选择,只能存储指针。一个显而易见的是当你创建像二叉树这样的东西时:

template <class T>
struct tree_node { 
    struct tree_node *left, *right;
    T data;
}

在这种情况下,定义基本上是递归的,并且您不知道树节点可能具有多少个后代。您几乎坚持(至少某些变体)存储指针,并根据需要分配后代节点。

还有像动态字符串这样的情况,在父对象中只有一个对象(或对象数组),但它的大小可以在你需要的范围内变化很大(至少提供给使用动态分配的可能性。对于字符串,小尺寸是常见的,有一个相当广泛使用的“短字符串优化”,其中字符串对象直接包括足够的空间用于字符串达到某个限制,以及一个指针允许动态分配,如果字符串超过该尺寸:

template <class T>
class some_string { 
    static const limit = 20;
    size_t allocated;
    size_t in_use;
    union { 
        T short_data[limit];
        T *long_data;
    };

    // ...

};

使用指针而不是直接存储子对象的一个​​不太明显的原因是为了异常安全。仅举一个明显的例子,如果你只在父对象中存储指针,那么通常可以为那些提供swap保证的对象提供nothrow变得微不足道:

template <class T>
class parent { 
    T *data;

    void friend swap(parent &a, parent &b) throw() { 
         T *temp = a.data;
         a.data = b.data;
         b.data = temp;
    }
};

只有几个(通常是有效的)假设:

  1. 指针有效,以及
  2. 分配有效指针永远不会抛出异常
  3. ...这个交换无条件地给予nothrow保证是微不足道的(即,我们可以说:“交换不会抛出”)。如果parent直接存储对象而不是指针,我们只能保证有条件地(例如,swap将抛出当且仅当T的复制构造函数或赋值运算符抛出时。“)

    对于C ++ 11,经常使用这样的指针(通常是?)可以很容易地提供一个非常有效的移动构造函数(也提供nothrow保证)。当然,使用指向(大部分)数据的指针不是唯一可能的快速移动构造路径 - 但它很容易。

    最后,在您提出问题时,我怀疑您会想到这些问题 - 涉及的逻辑并不一定表明您是应该使用自动分配还是动态分配。在这种情况下,它(显然)是一个判断调用。从纯粹的理论角度来看,在这些情况下你可能没有任何区别。然而,从实际的角度来看,它可以产生很大的不同。尽管C和C ++标准都不保证(或甚至提示)任何类型,但实际情况是,在大多数典型系统中,使用自动分配的对象将最终出现在堆栈中。在大多数典型系统(例如,Windows,Linux)上,堆栈仅限于可用内存的相当一小部分(通常在单位数到低位两位数兆字节的数量级)。

    这意味着如果 all 在任何给定时间可能存在的这些类型的对象可能超过几兆字节(或左右),则需要确保(至少大部分)数据是动态分配,而非自动分配。有两种方法可以做到这一点:您可以将它留给用户,以便在它们可能超出可用堆栈空间时动态分配父对象,否则您可以让用户使用分配的相对较小的“shell”对象代表用户动态创建空间。

    如果这可能是一个问题,那么类几乎总是优先处理动态分配而不是强迫用户这样做。这有两个明显的好处:

    1. 用户可以使用基于堆栈的资源管理(SBRM,又名RAII)和
    2. 有限的堆栈空间的影响是有限的,而不是通过整个设计“渗透”。
    3. 底线:特别是对于一个模板,其中存储的类型不是预先知道的,我倾向于支持指针和动态分配。我保留直接存储子对象主要是为了我知道存储类型(几乎?)总是非常小,或者分析表明动态分配导致实际速度问题的情况。然而,在后一种情况下,我至少会给出一些替代方法,例如为该类重载operator new