有效运用auto_ptr

翻译:elmar
本文发表于1999年10月份的C/C++ Users Journal, 17(10)
 
很多人听说过标准auto_ptr智能指针机制,但并不是每个人都天天使用它。这真是个遗憾,因为auto_ptr优雅地解决了C++设计和编码中常见的问题,正确地使用它可以生成健壮的代码。本文阐述了如何正确运用auto_ptr来让你的代码更加安全――以及如何避免对auto_ptr危险但常见的误用,这些误用会引发间断性发作、难以诊断的bug。
1.为什么称它为“自动”指针?
auto_ptr只是众多可能的智能指针之一。许多商业库提供了更复杂的智能指针,用途广泛而令人惊异,从管理引用的数量到提供先进的代理服务。可以把标准C++ auto_ptr看作智能指针的Ford Escort(elmar注:可能指福特的一种适合家居的车型):一个简易、通用的智能指针,它不包含所有的小技巧,不像专用的或高性能的智能指针那么奢华,但是它可以很好的完成许多普遍的工作,它很适合日常性的使用。
auto_ptr所做的事情,就是动态分配对象以及当对象不再需要时自动执行清理。这里是一个简单的代码示例,没有使用auto_ptr所以不安全:
    // 示例 1(a): 原始代码
    //
    void f()
    {
      T* pt( new T );
      /*...更多的代码...*/
      delete pt;
    }
我们大多数人每天写类似的代码。如果f()函数只有三行并且不会有任何意外,这么做可能挺好的。但是如果f()从不执行delete语句,或者是由于过早的返回,或者是由于执行函数体时抛出了异常,那么这个被分配的对象就没有被删除,从而我们产生了一个经典的内存泄漏。
能让示例1(a)安全的简单办法是把指针封装在一个“智能的”类似于指针的对象里,这个对象拥有这个指针并且能在析构时自动删除这个指针所指的对象。因为这个智能指针可以简单的当成一个自动的对象(这就是说,它出了作用域时会自动毁灭),所以很自然的把它称之为“智能”指针:
    // 示例 1(b): 安全代码, 使用了auto_ptr
    //
    void f()
    {
      auto_ptr<T> pt( new T );
      /*...更多的代码...*/
    } // 酷: 当pt出了作用域时析构函数被调用,
      // 从而对象被自动删除
现在代码不会泄漏T类型的对象,不管这个函数是正常退出还是抛出了异常,因为pt的析构函数总是会在出栈时被调用。清理会自动进行。
最后,使用一个auto_ptr就像使用一个内建的指针一样容易,而且如果想要“撤销”资源,重新采用手动的所有权,我们只要调用release():
    // 示例 2: 使用一个 auto_ptr
    //
    void g()
    {
      T* pt1 = new T;
      // 现在,我们有了一个分配好的对象
      // 将所有权传给了一个auto_ptr对象
      auto_ptr<T> pt2( pt1 );
      // 使用auto_ptr就像我们以前使用简单指针一样
      *pt2 = 12;       // 就像 "*pt1 = 12;"
      pt2->SomeFunc(); // 就像 "pt1->SomeFunc();"
      // 用get()来获得指针的值
      assert( pt1 == pt2.get() );
      // 用release()来撤销所有权
      T* pt3 = pt2.release();
      // 自己删除这个对象,因为现在
      // 没有任何auto_ptr拥有这个对象
      delete pt3;
    } // pt2不再拥有任何指针,所以不要
      // 试图删除它...ok,不要重复删除
最后,我们可以使用auto_ptr的reset()函数来重置auto_ptr使之拥有另一个对象。如果这个auto_ptr已经拥有了一个对象,那么,它会先删除已经拥有的对象,因此调用reset()就如同销毁这个auto_ptr,然后新建一个并拥有一个新对象:
    // 示例 3: 使用reset()
    //
    void h()
    {
      auto_ptr<T> pt( new T(1) );
      pt.reset( new T(2) );
        // 删除由"new T(1)"分配出来的第一个T
    } // 最后,pt出了作用域,
      // 第二个T也被删除了
 
 
 
 
 
 
我找到的原文:
http://www.gotw.ca/publications/using_auto_ptr_effectively.htm
 

Using auto_ptr Effectively

This article appeared in C/C++ Users Journal, 17(10), October 1999.
 
Most people have heard of the standard auto_ptr smart pointer facility, but not everyone uses it daily. That's a shame, because it turns out that auto_ptr neatly solves common C++ design and coding problems, and using it well can lead to more robust code. This article shows how to use auto_ptr correctly to make your code safer--and how to avoid the dangerous but common abuses of auto_ptr that create intermittent and hard-to-diagnose bugs.

Why Call It an "Auto" Pointer?

auto_ptr is just one of a wide array of possible smart pointers. Many commercial libraries provide more sophisticated kinds of smart pointers that can do wild and wonderful things, from managing reference counts to providing advanced proxy services. Think of the Standard C++ auto_ptr as the Ford Escort of smart pointers: A simple general-purpose smart pointer that doesn't have all the gizmos and luxuries of special-purpose or high-performance smart pointers, but that does many common things well and is perfectly suitable for regular daily use.
What auto_ptr does is own a dynamically allocated object and perform automatic cleanup when the object is no longer needed. Here's a simple example of code that's unsafe without auto_ptr:
    // Example 1(a): Original code
    //
    void f()
    {
      T* pt( new T );
      /*...more code...*/
      delete pt;
    }
Most of us write code like this every day. If f() is a three-line function that doesn't do anything exceptional, this may be fine. But if f() never executes the delete statement, either because of an early return or because of an exception thrown during execution of the function body, then the allocated object is not deleted and we have a classic memory leak.
A simple way to make Example 1(a) safe is to wrap the pointer in a "smarter" pointer-like object that owns the pointer and that, when destroyed, deletes the pointed-at object automatically. Because this smart pointer is simply used as an automatic object (that is, one that's destroyed automatically when it goes out of scope), it's reasonably called an "auto" pointer:
    // 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
Now the code will not leak the T object, no matter whether the function exits normally or by means of an exception, because pt's destructor will always be called during stack unwinding. The cleanup happens automatically.
Finally, using an auto_ptr is just about as easy as using a built-in pointer, and to "take back" the resource and assume manual ownership again, we just call 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
Finally, we can use auto_ptr's reset() function to reset the auto_ptr to own a different object. If the auto_ptr already owned an object, though, it first deletes the already-owned object, so calling reset() is much the same as destroying the auto_ptr and creating a new one that owns the new object:
    // 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

Wrapping Pointer Data Members

Similarly, auto_ptr can be used to safely wrap pointer data members. Consider the following common example that uses the Pimpl (or, compiler-firewall) 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_; }
In brief, C's private details are split off into a separate implementation object that's hidden behind an opaque pointer. The idea is that C's constructor is responsible for allocating the private helper "Pimpl" object that contains the class's hidden internals, and C's destructor is responsible for deallocating it. Using auto_ptr, however, we find an easier way:
    // 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 ) { }
Now the destructor doesn't need to worry about deleting the pimpl_ pointer, because the auto_ptr will handle it automatically. In fact, if there's no other reason for explicitly writing a destructor, we don't need to bother with a custom destructor at all any more. Clearly, this is easier than managing the pointer manually, and it follows the good practice of wrapping resource ownership in objects--a job that auto_ptr is well suited to do. We'll revisit this example again at the end.

Ownership, Sources, and Sinks

This is nifty stuff all by itself, but it gets better: It's also very useful to pass auto_ptrs to and from functions, as function parameters and return values.
To see why, first consider what happens when you copy an auto_ptr: An auto_ptr owns the object that it holds a pointer to, and only one auto_ptr may own an object at a time. When you copy an auto_ptr, you automatically transfer ownership from the source auto_ptr to the target auto_ptr; if the target auto_ptr already owns an object, that object is first freed. After the copy, only the target auto_ptr owns the pointer and will delete it in due time, while the source is set back to a null state and can no longer be used to refer to the owned object.
For example:
    // 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
But be careful to avoid the pitfall of trying to use a non-owning 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
    }
With that in mind, we start to see how well auto_ptr works with sources and sinks. A "source" is a function or other operation that creates a new resource, and then typically hands off and relinquishes ownership of the resource. A "sink" is a function that does the reverse, namely that takes ownership of an existing object (and typically disposes of it). Instead of just having sources and sinks return and take bald pointers, though, it's usually better to return or take a smart pointer that owns the resource:
    // 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
Note the elegance of what's going on here:
1.       Source() allocates a new object and returns it to the caller in a completely safe way, by letting the caller assume ownership of the pointer. Even if the caller ignores the return value (of course, you would never write code that ignores return values, right?), the allocated object will always be safely deleted.
At the end of this article, I'll demonstrate why returning an auto_ptr is an important idiom. It turns out that returning a result by wrapping it in something like an auto_ptr is sometimes the only way to make a function strongly exception-safe.
2.       Sink() takes an auto_ptr by value and therefore assumes ownership of it. When Sink() is done, the deletion is performed as the local auto_ptr object goes out of scope (as long as Sink() itself hasn't handed off ownership to someone else). The Sink() function as written above doesn't actually do anything with its parameter, so calling "Sink( pt );" is a fancy way of writing "pt.reset(0);", but normally a sink function would do some work with the object before freeing it.

Things Not To Do, and Why Not To Do Them

Beware: Never use auto_ptrs except in one of the ways I just described above. I have seen many programmers try to use auto_ptrs in other ways just as they would use any other object. The problem with this is that auto_ptrs are most assuredly not like any other object. Here's the fundamental issue, and I'll highlight it to make sure it stands out:
For auto_ptr, copies are NOT equivalent.
It turns out that this has important effects when you try to use auto_ptrs with generic code that does make copies and isn't necessarily aware that copies aren't equivalent (after all, usually copies are!).  Consider the following code that I regularly see posted on the C++ newsgroups:
    // Example 8: Danger, Will Robinson!
    //
    vector< auto_ptr<T> > v;
    /* ... */
    sort( v.begin(), v.end() );
It is never safe to put auto_ptrs into standard containers. Some people will tell you that their compiler and library compiles this fine, and others will tell you that they've seen exactly this example recommended in the documentation of a certain popular compiler; don't listen to them.
The problem is that auto_ptr does not quite meet the requirements of a type you can put into containers, because copies of auto_ptrs are not equivalent. For one thing, there's nothing that says a vector can't just decide to up and make an "extra" internal copy of some object it contains. For another, when you call generic functions that will copy elements, like sort() does, the functions have to be able to assume that copies are going to be equivalent. At least one popular sort internally takes a copy of a "pivot" element, and if you try to make it work on auto_ptrs it will merrily take a copy of the pivot auto_ptr object (thereby taking ownership and putting it in a temporary auto_ptr on the side), do the rest of its work on the sequence (including taking further copies of the now-non-owning auto_ptr that was picked as a pivot value), and when the sort is over the pivot is destroyed and you have a problem: At least one auto_ptr in the sequence (the one that was the pivot value) no longer owns the pointer it once held, and in fact the pointer it held has already been deleted!
So the standards committee bent over backwards to do everything it could to help you out: The Standard auto_ptr was deliberately and specifically designed to break if you try to use it with the standard containers (or, at least, to break with most natural implementations of the standard library). To do this, the committee used a trick: auto_ptr's copy constructor and copy assignment operator take references to non-const to the right-hand-side object. The standard containers' single-element insert() functions take a reference to const, and hence won't work with auto_ptrs.

Interlude: The const auto_ptr Idiom

One cute and intentional result of this engineering of auto_ptr is that const auto_ptrs never lose ownership: Copying a const auto_ptr is illegal, and in fact the only things you can do with a const auto_ptr are dereference it with operator*() or operator->() or call get() to inquire about the value of the contained pointer. This means that we have a clear and concise idiom to express that an auto_ptr can never lose ownership:
    // 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
Now that's what I call const! So if you want to declare to the world that an auto_ptr can never be changed and will always delete what it owns, this is the way to do it. The const auto_ptr idiom is a useful and common technique, and one that you should keep in mind.

auto_ptr and Exception Safety

Finally, auto_ptr is sometimes essential to writing exception-safe code. Consider the following function:
    // Example 10(a): Exception-safe?
    //
    String f()
    {
      String result;
      result = "some value";
      cout << "some output";
      return result;
    }
This function has two visible side effects: It emits some output, and it returns a String. A detailed examination of exception safety is beyond the scope of this article, [2] but the goal we want to achieve is the strong exception-safety guarantee, which boils down to ensuring that the function acts atomically--even if there are exceptions, either all side effects happen or none of them do.
Although the code in Example 10(a) comes pretty close to achieving the strong exception-safety guarantee, there's still one minor quibble, as illustrated by the following client code:
    String theName;
    theName = f();
The String copy constructor is invoked because the result is returned by value, and the copy assignment operator is invoked to copy the result into theName. If either copy fails, then f() has completed all of its work and all of its side effects (good), but the result has been irretrievably lost (oops).
Can we do better, and perhaps avoid the problem by avoiding the copy?   For example, we could let the function take a non- const String reference parameter and place the return value in that:
    // Example 10(b): Better?
    //
    void f( String& result )
    {
      cout << "some output";
      result = "some value";
    }
This may look better, but it isn't, because the assignment to result might still fail which leaves us with one side effect complete and the other incomplete. Bottom line, this attempt doesn't really buy us much.
One way to solve the problem is to return a pointer to a dynamically allocated String, but the best solution is to go a step farther and return the pointer in an 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
    }
This does the trick, since we have effectively hidden all of the work to construct the second side effect (the return value) while ensuring that it can be safely returned to the caller using only nonthrowing operations after the first side effect has completed (the printing of the message). We know that, once the cout is complete, the returned value will make it successfully into the hands of the caller, and be correctly cleaned up in all cases: If the caller accepts the returned value, the act of accepting a copy of the auto_ptr causes the caller to take ownership; and if the caller does not accept the returned value, say by ignoring the return value, the allocated String will be automatically cleaned up as the temporary auto_ptr holding it is destroyed. The price for this extra safety?   As often happens when implementing strong exception safety, the strong safety comes at the (usually minor) cost of some efficiency--here, the extra dynamic memory allocation. But, when it comes to trading off efficiency for correctness, we usually ought to prefer the latter!
Make a habit of using smart pointers like auto_ptr in your daily work. auto_ptr neatly solves common problems and will make your code safer and more robust, especially when it comes to preventing resource leaks and ensuring strong exception safety. Because it's standard, it's portable across libraries and platforms, and so it will be right there with you wherever you take your code.

Acknowledgments

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.
 

Notes

1. The Pimpl Idiom is useful for reducing project build times because it prevents wide-ranging recompilations of client code whenever the private portions of C change. For more about the Pimpl Idiom and how best to deploy compiler firewalls, see Items 26 to 30 in the book Exceptional C++ (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).

你可能感兴趣的:(C++,c,职场,休闲)