MyMSDN

MyMSDN记录开发新知道

[翻译]高效使用auto_ptr

本文来自C/C++用户日志,17(10),1999年10月  原文链接

大部分人都听说过auto_ptr指针,但是并非所有人都每天使用它。不使用它是不明智的(可耻的),因为auto_ptr的设计初衷是为了解决C++设计和编码的普遍问题,将它用好可以写出更健壮的代码。本文指出如何正确使用auto_ptr以使程序变得安全,以及如何避开危险,而不是一般使用auto_ptr的恶习所致的创建间歇性和难以诊断的问题。

为什么它是一个“自动”指针

auto_ptr只是许许多多智能指针中的一种。许多商业库提供许多更强大的智能指针,可以完成更多的事情。从可以管理引用计数到提供更先进的代理服务等。应该把auto_ptr认为是智能指针中的福特Escort[注释]:一个基于简单且通用目的的智能指针,既没有小发明也没有丰富的特殊目的更不需要高性能,但是能将许多普通的事情做好,并且能够适合日常使用的智能指针。

auto_ptr做这样一件事:拥有一个动态分配内存对象,并且在它不再需要的时候履行自动清理的职责。这里有个没有使用auto_ptr指针的不安全的例子:

    // Example 1(a): Original code
    //
    void f()
    {
      T* pt( new T );

      /*...more code...*/

      delete pt;
    }

我们每天都像这样写代码,如果f()只是一个三行程序,也没做什么多余的事情,这样做当然可以很好工作。但是如果f()没有执行delete语句,比如程序提前返回(return)了,或者在执行的时候抛出异常了,然后就导致已经分配的对象没有被删除,因此我们就有了一个经典的内存泄漏。

一个使Example(1)安全的办法是用一个“智能”的指针拥有这个指针,当销毁的时候,删除那个被指的自动分配的对象。因为这个智能指针被简单地用为自动对象(这就是,当它离开它的作用域的时候自动销毁对象),所以它被称作“自动”指针。

    // Example 1(b): Safe code, with auto_ptr
    //
    void f()
    {
      auto_ptr<T> pt( new T );

      /*...more code...*/

    } // cool: pt's destructor is called as it goes out
      // of scope, and the object is deleted automatically

现在这段代码将不会再T对象上发生泄漏了,不必在意这个方法是正常退出还是异常退出,因为pt的析构函数将总是在堆栈弹出的时候被调用。清理工作将自动进行。

最后,使用auto_ptr和使用内建指针一样地容易,如果要“收回”资源并且再次手动管理的话,我们可以调用release():

    // Example 2: Using an auto_ptr
    //
    void g()
    {
      T* pt1 = new T;
      // right now, we own the allocated object

      // pass ownership to an auto_ptr
      auto_ptr<T> pt2( pt1 );

      // use the auto_ptr the same way
      // we'd use a simple pointer
      *pt2 = 12;       // same as "*pt1 = 12;"
      pt2->SomeFunc(); // same as "pt1->SomeFunc();"

      // use get() to see the pointer value
      assert( pt1 == pt2.get() );

      // use release() to take back ownership
      T* pt3 = pt2.release();

      // delete the object ourselves, since now
      // no auto_ptr owns it any more
      delete pt3;

    } // pt2 doesn't own any pointer, and so won't
      // try to delete it... OK, no double delete

最后,我们可以使用auto_ptr的reset()方法将auto_ptr重置向另一个对象。如果auto_ptr已经获得一个对象,这个过程就像是它先删除已经拥有的对象,因此调用reset(),就像是先销毁了auto_ptr,然后重建了一个新的并拥有该新对象:

    // Example 3: Using reset()
    //
    void h()
    {
      auto_ptr<T> pt( new T(1) );

      pt.reset( new T(2) );
        // deletes the first T that was
        // allocated with "new T(1)"

    } // finally, pt goes out of scope and
      // the second T is also deleted

包装指针数据成员

同样,auto_ptr也可以被用于安全地包装指针数据成员。考虑下面使用Pimpl idiom(或者,编译器防火墙)的例子:[1]

    // Example 4(a): A typical Pimpl
    //

    // file c.h
    //
    class C
    {
    public:
      C();
      ~C();
      /*...*/
    private:
      class CImpl; // forward declaration
      CImpl* pimpl_;
    };

    // file c.cpp
    //
    class C::CImpl { /*...*/ };

    C::C() : pimpl_( new CImpl ) { }
    C::~C() { delete pimpl_; }

简单地说,就是C的私有细节被实现为一个单独的对象,藏匿于一个指针之中。该思路要求C的构造函数负责为隐藏在类内部的辅助“Pimpl”对象分配内存,并且C的析构函数负责销毁它。使用auto_ptr,我们会发现这非常容易:

    // Example 4(b): A safer Pimpl, using auto_ptr
    //

    // file c.h
    //
    class C
    {
    public:
      C();
      /*...*/
    private:
      class CImpl; // forward declaration
      auto_ptr<CImpl> pimpl_;
    };

    // file c.cpp
    //
    class C::CImpl { /*...*/ };

    C::C() : pimpl_( new CImpl ) { }

现在,析构函数不需要担心删除pimpl_指针了,因为auto_ptr将自动处理它。事实上,如果没有其它需要显式写析构函数的原因,我们完全不需要自定义析构函数。显然,这比手动管理指针要容易得多,并且将对象所有权包含进对象是一个不错的习惯,这正是auto_ptr所擅长的。我们将在最后再次回顾这个例子。

所有权,源,以及调用者(Sinks)

它本身很漂亮,并且做得非常好:从函数传入或传出auto_ptrs,是非常有用的,比如函数的参数或者返回值。

让我们看看为什么,首先我们考虑当拷贝auto_ptr的时候会发生什么:一个auto_ptr获得一个拥有指针的对象,并且在同一时间只允许有一个auto_ptr可以拥有这个对象。当你拷贝一个auto_ptr的时候,你自动将源auto_ptr的所有权,传递给目标auto_ptr;如果目标auto_ptr已经拥有了一个对象,这个对象将先被释放。在拷贝完之后,只有目标auto_ptr拥有指针,并且负责在合适的时间销毁它,而源将被设置为空(null),并且不能再被当作原有指针的代表来使用。

例如:

    // Example 5: Transferring ownership from
    //            one auto_ptr to another
    //
    void f()
    {
      auto_ptr<T> pt1( new T );
      auto_ptr<T> pt2;

      pt1->DoSomething(); // OK

      pt2 = pt1;  // now pt2 owns the pointer,
                  // and pt1 does not

      pt2->DoSomething(); // OK

    } // as we go out of scope, pt2's destructor
      // deletes the pointer, but pt1's does nothing

但是要避免陷阱再次使用已经失去所有权的auto_ptr:

    // Example 6: Never try to do work through
    //            a non-owning auto_ptr
    //
    void f()
    {
      auto_ptr<T> pt1( new T );
      auto_ptr<T> pt2;

      pt2 = pt1;  // now pt2 owns the pointer, and
                  // pt1 does not

      pt1->DoSomething();
                  // error! following a null pointer
    }

谨记于心,我们现在看看auto_ptr如何在源和调用者之间工作。“源”这里是指一个函数,或者其它创建一个新资源的操作,并且通常将移交出资源的所有权。一个“调用者”函数反转这个关系,也就是获得已经存在对象的所有权(并且通常还负责释放它)。而不是有一个源和调用者,返回并且利用一个秃头指针(译者注:而不是使用一个局部变量来传递这个指针),虽然,通过一个秃头指针来获得一个资源通常很好:

    // Example 7: Sources and sinks
    //

    // A creator function that builds a new
    // resource and then hands off ownership.
    //
    auto_ptr<T> Source()
    {
      return auto_ptr<T>( new T );
    }

    // A disposal function that takes ownership
    // of an existing resource and frees it.
    //
    void Sink( auto_ptr<T> pt )
    {
    }

    // Sample code to exercise the above:
    auto_ptr<T> pt( Source() ); // takes ownership

注意下面的微妙的变化:

  1. Source()分配了一个新对象并且以一个完整安全的方式将它返回给调用者,并让调用者成为指针的拥有着。即使调用者忽略了返回值(显然,如果调用者忽略了返回值,你应该从来没有写过代码来删除这个对象,对吧?),分配的对象也将被自动安全地删除。

    在本文的最后,我将演示返回一个auto_ptr是一个好习惯。让返回值包裹进一些东西比如auto_ptr通常是使得函数变得强健的有效方式。

  2. Sink()通过传值的方式获得对象所有权。当执行完Sink()的时候,当离开作用域的时候,删除操作将被执行(只要Sink()没有将所有权转移)。上面所写的Sink()函数实际上并没有对参数做任何事情,因此调用“Sink(pt);”就等于写了“pt.reset(0);”,但是大部分的Sink函数都将在释放它之前做一些工作。

不可以做的事情,以及为什么不能做

谨记:千万不要以我之前没有提到的方式使用auto_ptrs。我已经看见过很多程序员试着用其他方式写auto_ptrs就像他们在使用其它对象一样。但问题是auto_ptr并不像其他对象。这里有些基本原则,我将把它们提出来以引起你的注意:

For auto_ptr, copies are NOT equivalent. (复制auto_ptr将与原来的不相等)

当你试着在一般的代码中使用auto_ptrs的时候,它将执行拷贝,并且没有任何提示,拷贝是不相等的(结果,它确实就是拷贝)。看下面这段代码,这是我在C++新闻组经常看见的:

    // Example 8: Danger, Will Robinson!
    //
    vector< auto_ptr<T> > v;

    /* ... */

    sort( v.begin(), v.end() );

在标准容器中使用auto_ptrs总是不安全的。一些人可能要告诉你,他们的编译器或者类库能够很好地编译它们,而另一些人则告诉你在某一个流行的编译器的文档中看到这个例子,不要听他们的。

问题是auto_ptr并不完全符合一个可以放进容器类型的前提,因为拷贝auto_ptrs是不等价的。首先,没有任何东西说明,vector不能决定增加并制造出“扩展”的内部拷贝。再次,当你调用一个一般函数的时候,它可能会拷贝元素,就像sort()那样,函数必须有能力假设拷贝是等价的。至少一个流行的排序拷贝“核心”的元素,如果你试着让它与auto_ptrs一起工作的话,它将拷贝一份“核心”的auto_ptr对象(因此转移所有权并且将所有权转移给一个临时对象),然后对其余的元素也采取相同的方式(从现有成员创建更多的拥有所有权的auto_ptr),当排序完成后,核心元素将被销毁,并且你将遇到一个问题:这组序列里至少一个auto_ptr(也就是刚才被掉包的那个核心元素)不再拥有对象所有权,而那个真实的指针已经随着临时对象的销毁而被删除了!

于是标准委员会回退并希望做一些能够帮助你避免这些行为的事情:标准的auto_ptr被故意设计成当你希望在使用标准容器的时候使用它时打断你(或者,至少,在大部分的标准库实现中打断你)。为了达到这个目的,标准委员会利用这样一个技巧:让auto_ptr's的拷贝构造函数和赋值操作符的右值(rhs)指向非常量。因为标准容器的单元素insert()函数,需要一个常量作为参数,因此auto_ptrs在这里就不工作了。(译者注:右值不能赋值给非常量)

使用const auto_ptr是一个好习惯

将一个auto_ptr设计成const auto_ptrs将不再丢失所有权:拷贝一个const auto_ptr是违法的(译者注:没有这样的构造函数),实际上你可以针对它做的唯一事情就是通过operator*()或者operator->()解引用它或者调用get()来获得所包含的指针的值。这意味着我们有一个简单明了的风格来表达一个绝不丢失所有权的auto_ptr:

    // Example 9: The const auto_ptr idiom
    //
    const auto_ptr<T> pt1( new T );
        // making pt1 const guarantees that pt1 can
        // never be copied to another auto_ptr, and
        // so is guaranteed to never lose ownership

    auto_ptr<T> pt2( pt1 ); // illegal
    auto_ptr<T> pt3;
    pt3 = pt1;              // illegal
    pt1.release();          // illegal
    pt1.reset( new T );     // illegal

这就是我要说的cosnt!因此如果现在你要向世界证明你的auto_ptr是不会被改变并且将总是删除其所有权,加上const就是你要做的。const auto_ptr风格是有用的,你必须将它谨记于心。

auto_ptr以及异常安全

最后,auto_ptr对写出异常安全的代码有时候非常必要,思考下面的代码:

    // Example 10(a): Exception-safe?
    //
    String f()
    {
      String result;
      result = "some value";
      cout << "some output";
      return result;
    }

该函数有两个可见的作用:它输出一些内容,并且返回一个String。关于异常安全的详细说明超出了本文的范围[2],但是我们想要取得的目标就是强异常安全的保障,归结为确保函数的原子性——如果有异常,所有的作用一起发生或者都不发生。

虽然在例10(a)中的代码非常精巧,看起来相当接近于异常安全的代码,但仍然有一些小的瑕疵,就像下面的客户代码所示:

    String theName;
    theName = f();

因为结果通过值返回,因此String的拷贝构造函数将被调用,而拷贝赋值操作符被调用来将结果拷贝到theName中。如果任何一个拷贝失败了,f()就完成了所有它的工作以及所有它的任务(这很好),但是结果是无法挽回的(哎哟我的妈呀)

我们可以做的更好吗,是否可以通过避免拷贝来避免这个问题?例如,我们可以 让函数有一个非常量引用参数并向下面这样返回值:

    // Example 10(b): Better?
    //
    void f( String& result )
    {
      cout << "some output";
      result = "some value";
    }

这看起来很棒,但实际不是这样的,返回result的赋值的函数只完成了一个功能,而将其它事情留给了我们。它仍然会出错。因此这个做法不可取。

解决这个问题的一个方法是返回一个指向动态分配指针的String对象,但是最好的解决方案是让我们做的更多,返回一个指针包含在auto_ptr:

    // Example 10(c): Correct (finally!)
    //
    auto_ptr<String> f()
    {
      auto_ptr<String> result = new String;
      *result = "some value";
      cout << "some output";
      return result;  // rely on transfer of ownership;
                      // this can't throw
    }

这里是一个技巧,当我们有效隐藏所有的工作来构造第二个功能(返回值)当确保它可以被安全返回给调用者并且在第一个功能(打印消息)完成的时候没有抛出操作。我们知道一旦cout完成,返回值将成功交到调用者手中,并且无论如何都会正确清理:如果调用者接受返回值,调用者将得到这个拷贝的auto_ptr临时对象的所有权;如果调用者没有接受返回值,也就是忽略返回值,分配的String将在临时auto_ptr被销毁的时候自动清理。这种安全扩展的代价呢?就像我们经常实现的强异常安全一样,强安全通常消耗一些效率(通常比较小)——这里指额外的动态内存分配。但是当我们在效率和正确性之间做出选择的话,我们通常会选择后者!

让我们养成在日常工作中使用auto_ptr的习惯。auto_ptr解决了常见的问题,并且能够使你的代码变得更安全和健壮,特别是它可以防止内存泄漏以及确保强安全。因为它是标准的,因此它在不同类库和平台之间是可移植的,因此无论你在哪里使用它,它都将是对的。

致谢

This article is drawn from material in the new book Exceptional C++: 47 engineering puzzles, programming problems, and exception-safety solutions by Herb Sutter, © 2000 Addison Wesley Longman Inc., which contains further detailed treatments of points touched on briefly in this article, including exception safety, the Pimpl (compiler-firewall) Idiom, optimization, const-correctness, namespaces, and other C++ design and programming topics.

注释

  1. Pimpl风格可以有效减少项目构建时间,因为它在C私有部分改变的时候,阻止客户代码引起广泛的重新编译。更多关于Pimpl风格以及如何部署编译器墙,参考这本Exceptional C++的条款26到30。(Addison-Wesley, 2000)

  2. See the article "Exception-Safe Generic Containers" originally published in C++ Report and available on the Effective C++ CD (Scott Meyers, Addison-Wesley, 1999) and Items 8 to 19 in Exceptional C++ (Herb Sutter, Addison-Wesley, 2000).

posted on 2010-04-07 19:08 volnet 阅读(3571) 评论(10)  编辑 收藏 引用 所属分类: 知识库(KnowledgeLibrary)C/C++

评论

# re: [翻译]高效使用auto_ptr 2010-04-07 20:05 giscn

最好远离 auto_ptr, 这个东西引进来的问题比解决的问题多  回复  更多评论   

# re: [翻译]高效使用auto_ptr 2010-04-07 20:32 唐风

@giscn
给些例子或是一些链接不咧
我赞同本文的观点,但同时很想知道“远离 auto_ptr” 的理由是否也能说明我。

谢谢了。

  回复  更多评论   

# re: [翻译]高效使用auto_ptr 2010-04-08 13:02 Benjamin

什么时候用?该怎样用?这是关键  回复  更多评论   

# re: [翻译]高效使用auto_ptr 2010-04-08 13:40 giscn

首先如果要解决语句局部的内存自动回收,auto_ptr 采用了RAII的做法,这样很好,应该就到此为止,那些reset, release函数都不必引入,使用也很简单。
但是:auto_ptr很显然想解决更广范围的内存回收,比如:一个指针在多个容器里,因此它引入了“所有权”、“源”这几个概念,其实,有COM开发的经验的人应该很熟悉这个概念,典型的,BSTR的创建与销毁规则就是这种思路,这个概念带来的麻烦COM开发人员是清楚的,我觉得归根结底,在这种情况下,所有权不是那么容易分辨,举例说明一下:一个指针保存在两个容器中,那么哪一个是有所有权呢?更糟糕的是auto_ptr利用了常用的赋值语义来确定所有权,首先,它改变了赋值操作的习惯,常规的,a=b之后,b依然是有效的,而 auto_ptr却让b是无效的,这个就是是问题的根源。
其所要解决的问题根本没有解决,你依然无法确定指针的所有权应该在那个容器中、何时销毁。即使你清楚地知道,一个小的疏忽:赋值的顺序就会引入bug。
好与不好,自己实践、体会最重要。  回复  更多评论   

# re: [翻译]高效使用auto_ptr 2010-04-08 14:10 volnet

@Benjamin
这就是这篇文章的主题啊,除了之前提到过的几种形式,就不应该发明其它形式了……  回复  更多评论   

# re: [翻译]高效使用auto_ptr 2010-04-09 03:22 欲三更

我觉得auto_ptr是C++里面典型的“因为设想太过宏伟,从而产生问题”的范例。

事实上大多数人为什么会想到用一个包装过的指针?害怕内存泄漏。那么我们需要的就是一个确定会在某一个域结束时把自身携带对象析构掉的指针,就这么简单。那么我们就实现一个没有赋值功能的auto_ptr就好了,一了百了。

而且我觉得这里面有一个逻辑问题:假设我们在大括号开始处建立一个携带对象的指针,并且确定在大括号结束的时候对象会析构掉,那我们有什么理由把它传递给大括号之外的代码呢?  回复  更多评论   

# re: [翻译]高效使用auto_ptr 2010-04-09 10:42 volnet

@欲三更
这就是应该象是:
// stack <- dumb pointer
// dump pointer -> do()
// dump pointer -> hello();
// stack -> delete dumb pointer auto

该干嘛让他自己干嘛去,我们要做的无非就是将指针在开始的时候交给栈可管理的对象去管理……然后继续放任自由……  回复  更多评论   

# re: [翻译]高效使用auto_ptr 2010-04-10 04:07 dui

please use smart pointer  回复  更多评论   

# re: [翻译]高效使用auto_ptr 2010-04-12 00:12 anonymous

认识auto_ptr的作用及局限性,合理使用。不要动不动就远离,不存在一个完美的东西可以解决所有的问题。  回复  更多评论   

# re: [翻译]高效使用auto_ptr 2010-08-29 17:32 evening dresses

都每天使用它  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


特殊功能