本文來自C/C++用戶日志,17(10),1999年10月 原文鏈接
大部分人都聽說過auto_ptr指針,但是并非所有人都每天使用它。不使用它是不明智的(可恥的),因?yàn)閍uto_ptr的設(shè)計(jì)初衷是為了解決C++設(shè)計(jì)和編碼的普遍問題,將它用好可以寫出更健壯的代碼。本文指出如何正確使用auto_ptr以使程序變得安全,以及如何避開危險(xiǎn),而不是一般使用auto_ptr的惡習(xí)所致的創(chuàng)建間歇性和難以診斷的問題。
為什么它是一個“自動”指針
auto_ptr只是許許多多智能指針中的一種。許多商業(yè)庫提供許多更強(qiáng)大的智能指針,可以完成更多的事情。從可以管理引用計(jì)數(shù)到提供更先進(jìn)的代理服務(wù)等。應(yīng)該把a(bǔ)uto_ptr認(rèn)為是智能指針中的福特Escort[注釋]:一個基于簡單且通用目的的智能指針,既沒有小發(fā)明也沒有豐富的特殊目的更不需要高性能,但是能將許多普通的事情做好,并且能夠適合日常使用的智能指針。
auto_ptr做這樣一件事:擁有一個動態(tài)分配內(nèi)存對象,并且在它不再需要的時(shí)候履行自動清理的職責(zé)。這里有個沒有使用auto_ptr指針的不安全的例子:
// Example 1(a): Original code
//
void f()
{
T* pt( new T );
/*...more code...*/
delete pt;
}
我們每天都像這樣寫代碼,如果f()只是一個三行程序,也沒做什么多余的事情,這樣做當(dāng)然可以很好工作。但是如果f()沒有執(zhí)行delete語句,比如程序提前返回(return)了,或者在執(zhí)行的時(shí)候拋出異常了,然后就導(dǎo)致已經(jīng)分配的對象沒有被刪除,因此我們就有了一個經(jīng)典的內(nèi)存泄漏。
一個使Example(1)安全的辦法是用一個“智能”的指針擁有這個指針,當(dāng)銷毀的時(shí)候,刪除那個被指的自動分配的對象。因?yàn)檫@個智能指針被簡單地用為自動對象(這就是,當(dāng)它離開它的作用域的時(shí)候自動銷毀對象),所以它被稱作“自動”指針。
// 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
現(xiàn)在這段代碼將不會再T對象上發(fā)生泄漏了,不必在意這個方法是正常退出還是異常退出,因?yàn)閜t的析構(gòu)函數(shù)將總是在堆棧彈出的時(shí)候被調(diào)用。清理工作將自動進(jìn)行。
最后,使用auto_ptr和使用內(nèi)建指針一樣地容易,如果要“收回”資源并且再次手動管理的話,我們可以調(diào)用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已經(jīng)獲得一個對象,這個過程就像是它先刪除已經(jīng)擁有的對象,因此調(diào)用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
包裝指針數(shù)據(jù)成員
同樣,auto_ptr也可以被用于安全地包裝指針數(shù)據(jù)成員。考慮下面使用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的私有細(xì)節(jié)被實(shí)現(xiàn)為一個單獨(dú)的對象,藏匿于一個指針之中。該思路要求C的構(gòu)造函數(shù)負(fù)責(zé)為隱藏在類內(nèi)部的輔助“Pimpl”對象分配內(nèi)存,并且C的析構(gòu)函數(shù)負(fù)責(zé)銷毀它。使用auto_ptr,我們會發(fā)現(xiàn)這非常容易:
// 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 ) { }
現(xiàn)在,析構(gòu)函數(shù)不需要擔(dān)心刪除pimpl_指針了,因?yàn)閍uto_ptr將自動處理它。事實(shí)上,如果沒有其它需要顯式寫析構(gòu)函數(shù)的原因,我們完全不需要自定義析構(gòu)函數(shù)。顯然,這比手動管理指針要容易得多,并且將對象所有權(quán)包含進(jìn)對象是一個不錯的習(xí)慣,這正是auto_ptr所擅長的。我們將在最后再次回顧這個例子。
所有權(quán),源,以及調(diào)用者(Sinks)
它本身很漂亮,并且做得非常好:從函數(shù)傳入或傳出auto_ptrs,是非常有用的,比如函數(shù)的參數(shù)或者返回值。
讓我們看看為什么,首先我們考慮當(dāng)拷貝auto_ptr的時(shí)候會發(fā)生什么:一個auto_ptr獲得一個擁有指針的對象,并且在同一時(shí)間只允許有一個auto_ptr可以擁有這個對象。當(dāng)你拷貝一個auto_ptr的時(shí)候,你自動將源auto_ptr的所有權(quán),傳遞給目標(biāo)auto_ptr;如果目標(biāo)auto_ptr已經(jīng)擁有了一個對象,這個對象將先被釋放。在拷貝完之后,只有目標(biāo)auto_ptr擁有指針,并且負(fù)責(zé)在合適的時(shí)間銷毀它,而源將被設(shè)置為空(null),并且不能再被當(dāng)作原有指針的代表來使用。
例如:
// 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
但是要避免陷阱再次使用已經(jīng)失去所有權(quán)的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
}
謹(jǐn)記于心,我們現(xiàn)在看看auto_ptr如何在源和調(diào)用者之間工作。“源”這里是指一個函數(shù),或者其它創(chuàng)建一個新資源的操作,并且通常將移交出資源的所有權(quán)。一個“調(diào)用者”函數(shù)反轉(zhuǎn)這個關(guān)系,也就是獲得已經(jīng)存在對象的所有權(quán)(并且通常還負(fù)責(zé)釋放它)。而不是有一個源和調(diào)用者,返回并且利用一個禿頭指針(譯者注:而不是使用一個局部變量來傳遞這個指針),雖然,通過一個禿頭指針來獲得一個資源通常很好:
// 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()分配了一個新對象并且以一個完整安全的方式將它返回給調(diào)用者,并讓調(diào)用者成為指針的擁有著。即使調(diào)用者忽略了返回值(顯然,如果調(diào)用者忽略了返回值,你應(yīng)該從來沒有寫過代碼來刪除這個對象,對吧?),分配的對象也將被自動安全地刪除。
在本文的最后,我將演示返回一個auto_ptr是一個好習(xí)慣。讓返回值包裹進(jìn)一些東西比如auto_ptr通常是使得函數(shù)變得強(qiáng)健的有效方式。
-
Sink()通過傳值的方式獲得對象所有權(quán)。當(dāng)執(zhí)行完Sink()的時(shí)候,當(dāng)離開作用域的時(shí)候,刪除操作將被執(zhí)行(只要Sink()沒有將所有權(quán)轉(zhuǎn)移)。上面所寫的Sink()函數(shù)實(shí)際上并沒有對參數(shù)做任何事情,因此調(diào)用“Sink(pt);”就等于寫了“pt.reset(0);”,但是大部分的Sink函數(shù)都將在釋放它之前做一些工作。
不可以做的事情,以及為什么不能做
謹(jǐn)記:千萬不要以我之前沒有提到的方式使用auto_ptrs。我已經(jīng)看見過很多程序員試著用其他方式寫auto_ptrs就像他們在使用其它對象一樣。但問題是auto_ptr并不像其他對象。這里有些基本原則,我將把它們提出來以引起你的注意:
For auto_ptr, copies are NOT equivalent. (復(fù)制auto_ptr將與原來的不相等)
當(dāng)你試著在一般的代碼中使用auto_ptrs的時(shí)候,它將執(zhí)行拷貝,并且沒有任何提示,拷貝是不相等的(結(jié)果,它確實(shí)就是拷貝)。看下面這段代碼,這是我在C++新聞組經(jīng)常看見的:
// Example 8: Danger, Will Robinson!
//
vector< auto_ptr<T> > v;
/* ... */
sort( v.begin(), v.end() );
在標(biāo)準(zhǔn)容器中使用auto_ptrs總是不安全的。一些人可能要告訴你,他們的編譯器或者類庫能夠很好地編譯它們,而另一些人則告訴你在某一個流行的編譯器的文檔中看到這個例子,不要聽他們的。
問題是auto_ptr并不完全符合一個可以放進(jìn)容器類型的前提,因?yàn)榭截?span style="font-family: 'Courier New'" class="Code">auto_ptrs是不等價(jià)的。首先,沒有任何東西說明,vector不能決定增加并制造出“擴(kuò)展”的內(nèi)部拷貝。再次,當(dāng)你調(diào)用一個一般函數(shù)的時(shí)候,它可能會拷貝元素,就像sort()那樣,函數(shù)必須有能力假設(shè)拷貝是等價(jià)的。至少一個流行的排序拷貝“核心”的元素,如果你試著讓它與auto_ptrs一起工作的話,它將拷貝一份“核心”的auto_ptr對象(因此轉(zhuǎn)移所有權(quán)并且將所有權(quán)轉(zhuǎn)移給一個臨時(shí)對象),然后對其余的元素也采取相同的方式(從現(xiàn)有成員創(chuàng)建更多的擁有所有權(quán)的auto_ptr),當(dāng)排序完成后,核心元素將被銷毀,并且你將遇到一個問題:這組序列里至少一個auto_ptr(也就是剛才被掉包的那個核心元素)不再擁有對象所有權(quán),而那個真實(shí)的指針已經(jīng)隨著臨時(shí)對象的銷毀而被刪除了!
于是標(biāo)準(zhǔn)委員會回退并希望做一些能夠幫助你避免這些行為的事情:標(biāo)準(zhǔn)的auto_ptr被故意設(shè)計(jì)成當(dāng)你希望在使用標(biāo)準(zhǔn)容器的時(shí)候使用它時(shí)打斷你(或者,至少,在大部分的標(biāo)準(zhǔn)庫實(shí)現(xiàn)中打斷你)。為了達(dá)到這個目的,標(biāo)準(zhǔn)委員會利用這樣一個技巧:讓auto_ptr's的拷貝構(gòu)造函數(shù)和賦值操作符的右值(rhs)指向非常量。因?yàn)闃?biāo)準(zhǔn)容器的單元素insert()函數(shù),需要一個常量作為參數(shù),因此auto_ptrs在這里就不工作了。(譯者注:右值不能賦值給非常量)
使用const auto_ptr是一個好習(xí)慣
將一個auto_ptr設(shè)計(jì)成const auto_ptrs將不再丟失所有權(quán):拷貝一個const auto_ptr是違法的(譯者注:沒有這樣的構(gòu)造函數(shù)),實(shí)際上你可以針對它做的唯一事情就是通過operator*()或者operator->()解引用它或者調(diào)用get()來獲得所包含的指針的值。這意味著我們有一個簡單明了的風(fēng)格來表達(dá)一個絕不丟失所有權(quán)的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!因此如果現(xiàn)在你要向世界證明你的auto_ptr是不會被改變并且將總是刪除其所有權(quán),加上const就是你要做的。const auto_ptr風(fēng)格是有用的,你必須將它謹(jǐn)記于心。
auto_ptr以及異常安全
最后,auto_ptr對寫出異常安全的代碼有時(shí)候非常必要,思考下面的代碼:
// Example 10(a): Exception-safe?
//
String f()
{
String result;
result = "some value";
cout << "some output";
return result;
}
該函數(shù)有兩個可見的作用:它輸出一些內(nèi)容,并且返回一個String。關(guān)于異常安全的詳細(xì)說明超出了本文的范圍[2],但是我們想要取得的目標(biāo)就是強(qiáng)異常安全的保障,歸結(jié)為確保函數(shù)的原子性——如果有異常,所有的作用一起發(fā)生或者都不發(fā)生。
雖然在例10(a)中的代碼非常精巧,看起來相當(dāng)接近于異常安全的代碼,但仍然有一些小的瑕疵,就像下面的客戶代碼所示:
String theName;
theName = f();
因?yàn)榻Y(jié)果通過值返回,因此String的拷貝構(gòu)造函數(shù)將被調(diào)用,而拷貝賦值操作符被調(diào)用來將結(jié)果拷貝到theName中。如果任何一個拷貝失敗了,f()就完成了所有它的工作以及所有它的任務(wù)(這很好),但是結(jié)果是無法挽回的(哎喲我的媽呀)
我們可以做的更好嗎,是否可以通過避免拷貝來避免這個問題?例如,我們可以 讓函數(shù)有一個非常量引用參數(shù)并向下面這樣返回值:
// Example 10(b): Better?
//
void f( String& result )
{
cout << "some output";
result = "some value";
}
這看起來很棒,但實(shí)際不是這樣的,返回result的賦值的函數(shù)只完成了一個功能,而將其它事情留給了我們。它仍然會出錯。因此這個做法不可取。
解決這個問題的一個方法是返回一個指向動態(tài)分配指針的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
}
這里是一個技巧,當(dāng)我們有效隱藏所有的工作來構(gòu)造第二個功能(返回值)當(dāng)確保它可以被安全返回給調(diào)用者并且在第一個功能(打印消息)完成的時(shí)候沒有拋出操作。我們知道一旦cout完成,返回值將成功交到調(diào)用者手中,并且無論如何都會正確清理:如果調(diào)用者接受返回值,調(diào)用者將得到這個拷貝的auto_ptr臨時(shí)對象的所有權(quán);如果調(diào)用者沒有接受返回值,也就是忽略返回值,分配的String將在臨時(shí)auto_ptr被銷毀的時(shí)候自動清理。這種安全擴(kuò)展的代價(jià)呢?就像我們經(jīng)常實(shí)現(xiàn)的強(qiáng)異常安全一樣,強(qiáng)安全通常消耗一些效率(通常比較小)——這里指額外的動態(tài)內(nèi)存分配。但是當(dāng)我們在效率和正確性之間做出選擇的話,我們通常會選擇后者!
讓我們養(yǎng)成在日常工作中使用auto_ptr的習(xí)慣。auto_ptr解決了常見的問題,并且能夠使你的代碼變得更安全和健壯,特別是它可以防止內(nèi)存泄漏以及確保強(qiáng)安全。因?yàn)樗菢?biāo)準(zhǔn)的,因此它在不同類庫和平臺之間是可移植的,因此無論你在哪里使用它,它都將是對的。
致謝
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風(fēng)格可以有效減少項(xiàng)目構(gòu)建時(shí)間,因?yàn)樗贑私有部分改變的時(shí)候,阻止客戶代碼引起廣泛的重新編譯。更多關(guān)于Pimpl風(fēng)格以及如何部署編譯器墻,參考這本Exceptional C++的條款26到30。(Addison-Wesley, 2000)
-
See the article 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).