本文来自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
注意下面的微妙的变化:
-
Source()分配了一个新对象并且以一个完整安全的方式将它返回给调用者,并让调用者成为指针的拥有着。即使调用者忽略了返回值(显然,如果调用者忽略了返回值,你应该从来没有写过代码来删除这个对象,对吧?),分配的对象也将被自动安全地删除。
在本文的最后,我将演示返回一个auto_ptr是一个好习惯。让返回值包裹进一些东西比如auto_ptr通常是使得函数变得强健的有效方式。
-
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.
注释
-
Pimpl风格可以有效减少项目构建时间,因为它在C私有部分改变的时候,阻止客户代码引起广泛的重新编译。更多关于Pimpl风格以及如何部署编译器墙,参考这本Exceptional C++的条款26到30。(Addison-Wesley, 2000)
-
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).