34

只是想对一个设计问题发表意见。如果你有一个 C++ 类而不是其他对象,你会使用智能指针来实现这一点吗?

class Example {
public: 
  // ...

private:
  boost::scoped_ptr<Owned> data;
};

“拥有”对象不能按值存储,因为它可能会在对象的整个生命周期中发生变化。

我的观点是,一方面,您明确该对象是拥有的并确保将其删除,但另一方面,您可以轻松地拥有一个常规指针并在析构函数中将其删除。这是矫枉过正吗?

跟进:只是想对您的所有回答表示感谢。感谢您对 auto_ptr 在复制整个对象时为另一个对象留下一个 NULL 指针的提醒,我已经广泛使用了 auto_ptr 但还没有想到这一点。除非我有充分的理由,否则我基本上将所有课程都设为 boost::noncopyable,因此无需担心。还要感谢有关异常中内存泄漏的信息,这也很高兴知道。我尽量不在构造函数中写可能导致异常的东西——有更好的方法——所以这不应该是一个问题。

不过,我还有另一个问题。当我问这个问题时,我想知道是否有人真的这样做了,你们似乎都提到理论上这是一个好主意,但没有人说他们真的这样做了。这让我感到惊讶!当然,一个对象拥有指向另一个对象的指针并不是一个新想法,我原以为你们都曾在某个时候做过。这是怎么回事?

4

7 回答 7

40

scoped_ptr 非常适合这个目的。但是必须了解它的语义。您可以使用两个主要属性对智能指针进行分组:

  • 可复制:可以复制智能指针:副本和原始共享所有权。
  • 可移动:可以移动智能指针:移动结果将拥有所有权,原来的将不再拥有。

这是相当常见的术语。对于智能指针,有一个特定的术语可以更好地标记这些属性:

  • 所有权转移:智能指针是可移动的
  • 所有权份额:智能指针是可复制的。如果智能指针已经是可复制的,则很容易支持所有权转移语义:那只是一个原子复制和重置原始操作,将其限制为某些类型的智能指针(例如,仅临时智能指针)。

(C)opyable让我们使用, 和(M)ovable,对可用的智能指针进行分组(N)either

  1. boost::scoped_ptr: N
  2. std::auto_ptr: 米
  3. boost::shared_ptr: C

auto_ptr有一个大问题,它使用复制构造函数来实现 Movable 的概念。这是因为当 auto_ptr 被 C++ 接受时,与新的 C++ 标准相反,还没有一种方法可以使用移动构造函数本地支持移动语义。也就是说,您可以使用 auto_ptr 执行以下操作,并且它可以工作:

auto_ptr<int> a(new int), b;
// oops, after this, a is reset. But a copy was desired!
// it does the copy&reset-of-original, but it's not restricted to only temporary
// auto_ptrs (so, not to ones that are returned from functions, for example).
b = a; 

无论如何,正如我们所看到的,在您的情况下,您将无法将所有权转移给另一个对象:您的对象实际上是不可复制的。在下一个 C++ 标准中,如果您使用 scoped_ptr,它将是不可移动的。

要使用 scoped_ptr 实现您的类,请注意您是否满足以下两点之一:

  • 在类的 .cpp 文件中写一个析构函数(即使它是空的),或者
  • 制作Owned一个完全定义的类。

否则,当你创建一个 Example 的对象时,编译器会为你隐式定义一个析构函数,它会调用 scoped_ptr 的析构函数:

~Example() { ptr.~scoped_ptr<Owned>(); }

然后,如果您没有完成上述两点中的任何一项,这将使 scoped_ptr callboost::checked_delete抱怨不完整。Owned如果您在 .cpp 文件中定义了自己的 dtor,则对 scoped_ptr 的析构函数的隐式调用将从 .cpp 文件中进行,您可以在其中放置Owned类的定义。

您对 auto_ptr 有同样的问题,但还有一个问题:为 auto_ptr 提供不完整的类型目前是未定义的行为(可能会在下一个 C++ 版本中修复)。因此,当您使用 auto_ptr 时,您必须在头文件中使 Owned 成为一个完整的类型。

shared_ptr 没有这个问题,因为它使用了一个多态删除器,它间接调用了删除。所以deleting函数不是在析构函数被实例化的时候被实例化,而是在shared_ptr的构造函数中创建deleter的时候。

于 2009-02-01T19:09:58.327 回答
30

是个好主意。它有助于简化您的代码,并确保当您在对象的生命周期内更改 Owned 对象时,前一个对象会被正确销毁。

但是,您必须记住 scoped_ptr 是不可复制的,这使得您的类默认情况下不可复制,直到/除非您添加自己的复制构造函数等。(当然,在原始指针的情况下使用默认复制构造函数是不行的 -也没有!)

如果您的类有多个指针字段,那么在一种情况下使用 scoped_ptr 实际上可以提高异常安全性:

class C
{
  Owned * o1;
  Owned * o2;

public:
  C() : o1(new Owned), o2(new Owned) {}
  ~C() { delete o1; delete o2;}
};

现在,想象一下在构造 C 的过程中,第二个“new Owned”抛出异常(例如内存不足)。o1 将被泄露,因为 C::~C() (析构函数)不会被调用,因为对象尚未完全构造。但是,任何完全构造的成员字段的析构函数都会被调用。因此,使用 scoped_ptr 而不是普通指针将允许 o1 被正确销毁。

于 2009-02-01T12:59:36.763 回答
8

这根本不是矫枉过正,这是一个好主意。

不过,它确实需要您的班级客户了解 boost。这可能是也可能不是问题。对于可移植性,您可以考虑 std::auto_ptr (在这种情况下)执行相同的工作。由于它是私有的,因此您不必担心其他人试图复制它。

于 2009-02-01T11:52:47.803 回答
5

使用 scoped_ptr 是个好主意。

保持和手动销毁指针并不像你想象的那么简单。特别是如果您的代码中有多个 RAW 指针。如果异常安全和不泄漏内存是优先事项,那么您需要大量额外的代码才能使其正确。

首先,您必须确保正确定义所有四种默认方法。这是因为这些方法的编译器生成的版本对于普通对象(包括智能指针)来说很好,但在正常情况下会导致指针处理问题(寻找浅拷贝问题)。

  • 默认构造函数
  • 复制构造函数
  • 赋值运算符
  • 析构函数

如果您使用 scoped_ptr,那么您无需担心任何这些。

现在,如果您的类中有多个 RAW 指针(或构造函数的其他部分可以抛出)。您必须在构造和销毁过程中明确处理异常。

class MyClass
{
    public:
        MyClass();
        MyClass(MyClass const& copy);
        MyClass& operator=(MyClass const& copy);
        ~MyClass();

    private
        Data*    d1;
        Data*    d2;
};

MyClass::MyClass()
    :d1(NULL),d2(NULL)
{
    // This is the most trivial case I can think off
    // But even it looks ugly. Remember the destructor is NOT called
    // unless the constructor completes (without exceptions) but if an
    // exception is thrown then all fully constructed object will be
    // destroyed via there destructor. But pointers don't have destructors.
    try
    {
        d1 = new Data;
        d2 = new Data;
    }
    catch(...)
    {
        delete d1;
        delete d2;
        throw;
    }
}

看看 scopted_ptr 是多么容易。

于 2009-02-01T17:25:38.677 回答
4

作用域指针正好适合这一点,因为它们确保对象被删除,而您作为程序员不必担心它。我认为这是对作用域 ptr 的一个很好的使用。

我发现一个好的设计策略通常是尽可能避免手动释放内存,让你的工具(在这种情况下是智能指针)为你做。正如我所看到的,手动删除是不好的一个主要原因,那就是代码变得难以快速维护。内存分配和释放的逻辑在代码中通常是分开的,这导致互补的行没有一起维护。

于 2009-02-01T11:53:06.617 回答
4

我认为这并不过分,这比使用原始指针更好地记录了成员的语义,并且不易出错。

于 2009-02-01T11:53:06.973 回答
3

何为矫枉过正?boost::scoped_ptr 很容易优化,我敢打赌,生成的机器代码与手动删除析构函数中的指针一样。

scoped_ptr 很好 - 只需使用它:)

于 2009-02-01T18:07:37.420 回答