对话#25:Getting to the Point(WQ译)
“啊!”
这一个小时内我第七次受挫后的大叫。 温迪明显不胜其扰了。”瞧瞧,朋友,安静一下,好吧?”她的声音飘过隔间。我觉得她的反应比以前好多了。
我以前就和auto_ptrs搏斗过了。在为Guru干活的最初几天内,我已经领教过auto_ptrs的价值了-以及它那不显眼的陷阱[1]. 我已经被auto_ptr那奇怪的所有权问题扎过一次了。auto_ptr使用伪装为拷贝操作的移交操作。想像一下这种情况: 你在操作一个复印机,放上你的表格,然后按 “拷贝”按钮。过了一会儿,复印机给你副本-然后将原件送入碎纸器。这就是auto_ptr的拷贝和赋值操作干的好事。
无论如何,我现在已经更聪明和更富有经验了,可仍然正陷入在auto_ptr的形形色色的限制之中。比如说,auto_ptr不能用来包容指向数组的指针。我玩弄了一个Hacking的技巧来绕开它,通过使用显式的数组delete:
auto_ptr<T> autoArray(new T[10]);
//...
delete [] autoArray.release;()
但立刻就后悔了。我可以预见到这个 “面目可憎的东西”(Guru会也会这么称呼它) 的许多危险。最重要的是,嗯,auto_ptr的卖点就是拥有和销毁内存;手工这么做完全打破了使用它的本意。
因此,我决定改用一个vector来拥有数组。它比我所需要的还更强劲一些,因为我不需要动态更改vector的大小,而它提供了此功能。 我实际上想要是一个能自动处理指针和内存管理的灵巧指针,并且可以用来处理数组。
“使用Boost库, 天行者,”我听到身后Guru的声音。
“是不是‘使用大场,天行者’?” 我侧肩答道。 “而且,不要叫我天行者。”
“Boost是能量的源泉,”Guru继续于她的电影台词之中。
我今天没心情,于是我打断了她。”嗯,我们能今天不用演戏吗?鲍伯在伦敦的办公室里,也没有新员神情反常。”
她来到附近,我看见了她的微笑和耸肩。”哦,我仅仅感到无聊,”她叹了口气。”你看过Boost库中的灵巧指针了吗?”
“嗯,”我吞吞吐吐的,”我还没空仔细去看Boost库。它怎么实现灵巧指针的?”
“共有五种灵巧指针,”Guru一边坐下一边说着。”两个处理指针的单所有权,两个处理共享所有权。”
“共享所有权?哦,类似于带引用计数的类?”
“完全正确。灵巧指针是成对的,一个处理指向单一物体的指针,另一个处理指向数组的指针。”在说的时候,她在白板上写下:scoped_ptr,scoped_array,shared_ptr和 shared_array。
“只有四个,你前面说有五个?”我问。
“剩下的一个叫weak_ptr。它是shared_ptr的非拥有关系的观察者。我随后讲它。 scoped_*形灵巧指针在它们离开生存范围时,自动析构所指向的对象。一个提议用途是实现Pimpl惯用法-指向实现的指针,”在我发问前,她急忙加了一句。
“如此说来...他们象auto_ptr,对吧?”
“不十分象。scoped_ptr和scoped_array不可拷贝。”
“不可拷贝?那么,如果我将一个指针作为类的成员-”我边说边在白板上潦草地写着:
class T
{
scoped_ptr<TImpl> p;
...
“-我如何实现拷贝和赋值?”
“和auto_ptr一样,”Guru边答边写:
T( const T& other )
: p(new TImpl(*other.p)
{
}
T& operator=( const T& other )
{
scoped_ptr<TImpl> tmp(new TImpl(*other.p));
p.swap;(tmp)
}
“Ooookay,”我懒洋洋地说着,装作我已经懂了。“ 于是,当我使用scoped_ptr的时候, 我必须同样完成使用auto_ptr时所必须做的所有工作,对吧?那么为什么不就使用auto_ptr?”
“因为使用auto_ptr时能编译,但是做错事, 因为自动生成的函数。 使用 scoped_ptr 使得极难忽略拷贝语义,因为如果你使用编辑器自动生成的版本时,编辑器将会拒绝编译这个类。同样,scoped_ptr不能够在一个不完全的类型上被使用。”我给予了Guru我擅长的车前灯前的鹿的目光。 她叹了口气。“考虑这种情况,”她边说边在白板上写:
class Simple;
Simple * CreateSimple();
void f()
{
auto_ptr<Simple>autoS(CreateSimple());
scoped_ptr<Simple>scopedS(CreateSimple());
//...
}
“在函数体中,Simple是一个不完全的类型。如果它的析构函数是有行为的,那么通过auto_ptr或scoped_ptr销毁它,其结果为未定义。一些编译器在你实例化auto_ptr时将会警告你这一点,但不是所有编译器都会这么做。相对的,scoped_ptr为确保语义正确而做了些小动作以造成编译错误,如果是在实例化一个不完全类型的话。”
“哦,是的,当然,” 我来劲了,以显示我理解了。 “因此我们应该始终使用scoped_ptr来代替auto_ptr,对吧?”
“错,抱歉,” 她失望地说道。“auto_ptr仍然有它的用处。不像auto_ptr,scoped_ptr 没有release()成员-它不能够放弃指针的所有权。 因为这个,以及因为scoped_ptr不能够被复制,当scoped_ptr离开生存范围时,它所管理的指针总是被delete。这使得scoped_ptr不适用于需要传递所有权的地方,比如厂。”
“使用scoped_ptr向其他程序员表明你的意图。它告诉其他人,‘这个指针不应该在当前范围外被复制’。正相反,auto_ptr允许从产生指针的代码空间向外传递所有权,但是它维持对指针的控制除非所有权的传递是完全地。 这当然对写异常安全的代码有重要意义。”
“啊,好,我明白了,”我嘟囔着。“ 你提到的另一种灵巧指针是怎么回事?”
“scoped_array的行为和scoped_ptr相同,除了它处理是对象数组而不是单个对象。 scoped_array 提供了稍有差别的访问函数。它没有operator*或operator->,但它有operator[]。我相信,”她总结道,“你需要是一个 scoped_array。”
“也许吧,”我答道,”但是我认为在作决定之前我应该多了解些shared_*形的灵巧指针。”
“非常明智,我的徒……弟,抱歉,叫习惯了。”Guru歉意地笑了一下。“是的, 熟悉各个选择之后再作决定是个好习惯。”
她指着白板上的shared_ptr 和 shared_array说道:“shared_*形的灵巧指针是非常有用的工具。 他们是引用计数型的指针,能够区分出所有者和观察者。举例来说, 使用上面的class T:”
void sharing()
{
shared_ptr<T> firstShared(new T);
shared_ptr<T> secondShared;(firstShared)
//...
}
“两个shared_ptr对象指向相同的T对象。引用计数现在是2。既然shared_ptr是引用计数的,对象一直不被销毁直到最后一个shared_ptr离开生存范围-此时引用计数降为0。shared_*的灵巧指针有相当的柔性,你可以使用它们包容指向不完全类型的指针。”
“我想你说过的,这很糟吧?”
“是很糟,如果你不小心的话。 shared_*的灵巧指针可以用一个指向不完全类型的指针来实例化,但必须要指明一个函数或函数子供销毁被拥有对象时调用。比如说,我们修改了上面的f函数:”
void DestroySimple( Simple* );
void f()
{
shared_ptr<Simple>sharedSFails( CreateSimple() );
shared_ptr<Simple>sharedSSucceeds( CreateSimple(), DestroySimple );
}
“第一个shared_ptr失败了, 因为Simple是一个不完全类型。第二个成功了,因为你明确地告诉了 shared_ptr 该如何销毁这个指针。
“因为 shared_ 指针被设计可被拷贝,它们完全适用于标准容器, 包括associative 容器。并且,在那些必须强制类型转换的特别场合上,可以定义特别的类型转换操作以生成新的shared_*形的灵巧指针。 例如,如果我们有一个类Base以及公有继承而来的Derived,和一个无关类,那么你将遇到:”
void g()
{
shared_ptr<Base>sharedBase( new Derived );
shared_ptr<Derived> sharedDer =
shared_dynamic_cast<Derived>( sharedBase);
shared_ptr<Unrelated> sharedUnrelated =
shared_dynamic_cast<Unrelated>( sharedBase);
try
{
shared_ptr<Unrelated> oops =
shared_polymorphic_cast<Unrelated>( sharedBase);
}
catch( bad_cast)
{
//...
}
}
“同样还存在着一个shared_static_cast,供那些特别场合使用,”Guru总结道。
我对这个函数研究了一会儿。“好吧,让我试试是否能推算出将发生什么。第一个shared_dynamic_cast在灵巧指针上执行了一个dynamic_cast,返回一个新的灵巧指针。 假如dynamic_cast失败了怎么办-在引用计数上将发生什么?”
Guru满意地点点头。“在那种情况下,原始的计数不被影响,而且新的hared_ptr包容的是一个NULL指针。正如你从try/catch语句推测的,shared_polymorphic_cast将试图在被容纳的指针上执行dynamic_cast。 如果转换失败,它将抛出bad_cast异常。”
“哇,”我赞叹道,“这个类可真完备。看起来很不错。”
“的确,”Guru同意。 “但还有些事情是必须要知道的。引用计算过于简单而不能检测任何形式的循环引用。同样,shared_ptr也没有实现任何形式的写时拷贝(copy-on-write),因此用它来实现Pimpl惯用法时必须仔细衡量。”
我深思熟虑后确定我喜欢它。“嗨,”我追问道,我还记着呢,“你提到过的weak_ptr是怎么回事?”
“啊,是的。weak_ptr与shared_ptrs联合使用。weak_ptr是观察者,不影响共享对象的引用计数。weak_ptr的主要目的是允许shared_ptr参与循环依赖-A引用B,B反过来又引用A。我喜欢将它想象成俱乐部中的准会员-它没有投票权,但是能参加俱乐部的会议。”
“Hmmm……” 我仔细想了一下。“因为 weak_ptr不影响引用计数,如果俱乐部解散了怎么办-也就是说,最後一个shared_ptr离开了生存范围-而此时 weak_ptr正在使用共享对象?”
“在那情况下,weak_ptr维护的指针被设置为NULL。而对于NULL,不能进行任何dereferencing操作,例如operator*、operator->、或operator[],在dereferencing指针前应该进行NULL检查。于是,正如dereferencing内建指针前必须进行NULL检查,你也一定要检查灵巧指针是否为NULL。这个限制适用于Boost库中的所有灵巧指针。”
“天那,要记太多的东西了,”我一边说一边盯着潦草地写在白板上的东西。
“别担心,”Guru微笑着站起离开了。“我会email给你Boost库的URL,以及练习和示范各种不同指针的一个小程序。”她当然说到做到,几分钟内我收到了她的email。 我读到这句时立即就闭上了眼睛:
“我的徒弟,如前所说,这是Boost库中的说明文档: <www.boost.org/libs/smart_ptr/smart_ptr.htm>[2]。 阅读并思考附上问题[3]。”
[感谢]
Thanks to Peter Dimov and Bjorn Karlsson for providing valuable comments and updates.
[注释]
[1] Jim Hyslop and Herb Sutter. “Conversations #1,” C++ Report, April 2000.
[2] The most recent version of the library and documentation can also be obtained via anonymous CVS checkout from the Source Forge project. Detailed instructions can be found at: <http://sourceforge.net/projects/boost/>.
[3] The sample code can be downloaded from the CUJ website: hyslop.zip.