天天看點

auto_ptr_ref的奇妙

auto_ptr是目前C++标準中唯一的一個智能指針(smart pointer),主要是用來自動管理指針所指向的記憶體資源。資源管理是程式設計中非常重要的一部分。資源(resource)是計算機中很寬泛的一個概念,用來表示程式中數量有限,用完就必須歸還的東西,比如常見的互斥鎖(mutex lock)、檔案指針、Win32中的畫刷(brush)……,其中記憶體(memory)是最常見的一種資源,也是我們最先接觸,使用最多的一種資源,是以它的地位至關重要。它的地位到底重要到什麼程度?對此有兩種截然不同的理念:

1.記憶體資源是如此的重要,以至于我們不能把它們交給計算機來管理,而必須由程式員來管理。

2.記憶體資源是如此的重要,以至于我們不能把它們交給程式員來管理,而必須由計算機來管理。

Java、C#、Eiffel秉承了第二種理念,把記憶體資源交給計算機管理,避免了程式員在記憶體管理上易犯的大量錯誤,從整體上提高了開發的效率,但是是以損失一定執行時間上的效率為代價的。是以這些語言在實時系統、作業系統、語言編譯器……等一些對時間要求比較嚴格的領域中運用的很少。

C語言秉承了第一種理念,C++也随之繼承了這種理念,進而也就把記憶體資源管理交給了程式員,對于高段C程式員,當然是給了他們更多的靈活性,可以編制出非常精美的藝術品,但是對于初學者和不那麼高段的C程式員,記憶體管理卻是麻煩的根源,帶給他們更多的不是靈活性,而是揮之不去的連環噩夢。比如記憶體洩漏(memory leak)、野指針(wild pointer)等導緻的一些極難察覺的bug,最後的調試除錯可能會讓我們覺得世界末日到了。【注1】

注1:不是有一種玩笑說法嗎?真正的程式員用C(或C++),我想,難度頗高的,由程式員本人負責的記憶體管理可能是支援這個觀點的一個重要理由。:)

但是在C++中有了另外一個管理記憶體(甚至資源)的選擇,那就是智能指針(smart pointer),資源擷取即初始化(resource acquisition is initialization)理念的具體實作。【注2】

注2:在C++的準标準Boost庫中,引入了幾個新的智能指針scoped_ptr、scoped_array、shared_ptr、shared_array、weak_ptr,相對于auto_ptr有它們的許多好處,感興趣的讀者可以到www.boost.org去看一看。Andrei Alexandrescu在他的Loki庫中也專門用Policy設計實作了一個可以擴充的SmartPtr模闆,裡面用到的一些技術還是很有價值的,可以到http://sourceforge.net/projects/loki-lib/下載下傳Loki庫閱讀。

标準中auto_ptr的引入主要就是為了解決記憶體資源管理這個讓人難以馴服的怪獸。不過在解決一個問題的同時,也會帶來一些新的問題,auto_ptr本身有擁有權(ownership)的轉移(transfer)問題,而且它本身不支援“值(value)語義”概念【注3】,是以不能用在STL容器裡面作為元素使用,在比較新的編譯器中,如果這樣使用的話,編譯器會阻止你。但是在稍微老一點的編譯器中使用的話,很可能會無風無浪的編譯通過,在執行的過程中,那麼我恭喜你,其時你正在一個很長的雷區裸奔,能夠毫發無損通過的機率那就隻有天知道了。:)

注3:關于這些概念,可以參考我寫的《範式的二維平面》。

auto_ptr本身的正确使用在很多書中有詳細的講解和示例,而且我們也可以直接閱讀auto_ptr的源代碼獲得更直覺的感受。是以對于它的使用我不想再浪費時間和筆墨,在auto_ptr目前實作中【注4】,我們會看到一個奇怪的類模闆auto_ptr_ref,第一次我在閱讀《The C++ Standard Library》的時候,看到講解auto_ptr的時候提到auto_ptr_ref就百思不得其解,說實話,這本書是寫得非常清晰易懂的,不過我覺得在auto_ptr這個地方花的筆墨比較吝啬,沒有完全把問題講清楚。而且我看的很多書、文章上也并沒有詳細講解這個auto_ptr_ref問題,今天我想來對此深入探讨一下,希望能夠抛磚引玉。

注4:auto_ptr的早期實作中有一些bug,後來Nicolai M. Josuttis 對此認真修正了,并作出了一個實作。讀者可以到http://www.josuttis.com/libbook/util/autoptr.hpp.html檢視。代碼本身并不長,我将它們全列在最下面了。讀者最好對照代碼看文章。其中關于成員函數模闆,我并沒有講解,很多書上都有,主要是為了解決指針之間的轉化,特别是對于多态指針。

auto_ptr_ref就像它的名字一樣,把一個值轉化為引用(reference),具體的說,也就是把一個右值(rvalue)轉化為一個左值(lvalue)。【注5】我們可能會很奇怪,C++什麼時候還需要用到這種功能?很不幸,為了保證auto_ptr的正确行為和一定的安全性,需要用到這個功能。

注5:到底什麼是左值,什麼是右值?有許多讓人混淆,并不明晰的概念表述,我會在下一篇文章中表明我的觀點。

我們都知道,auto_ptr在進行複制操作(assignment operator and copy constructor)的時候,資源的擁有權(ownership)會發生轉移,也即原來的auto_ptr所指向的記憶體資源轉給了新的auto_ptr,本身已經為空。是以說auto_ptr不支援“值語義”,即被複制的值不會改變。一個例子可能更能說明問題:

auto_ptr<int> ap1(new int(9));

auto_ptr<int> ap2(ap1);// ap1失去擁有權,現在指向空,ap2現在獲得指9的//記憶體資源

ap1 = ap2;//ap2失去擁有權,現在指向空,ap1現在獲得指向9的記憶體資源

我們在觀察auto_ptr的assignment operator、copy constructor的實作時,也能夠發現它的參數是auto_ptr& rhs,而不是auto_ptr const& rhs【注6】。也就是說,auto_ptr進行複制操作的時候,它的引數(argument)必須是一個可以改變的左值(事實是這個值必定被改變)。

注6:這種寫法你可能不習慣,其實就是const auto_ptr& rhs,我為什麼要用這種寫法,可以參考我寫的《C之詭谲(下)》,就知道我并不是為了标新立異。:)

我們最常見到的,複制操作的參數類型都是引用到常量(reference to const),這正好是為了避免改變傳進來的引數(argument)。由于不會改變被引用的值,是以C++标準規定:常量引用可以引用右值。比如下列代碼都是合法的:

int const& ri = 60;//60是右值

list<int> const& rl = list<int>();//list<int>()是右值

int fun(){…}; int const& rf = fun();//fun()是右值

但是一般引用(非常量引用)是絕對不可以引用右值的。比如下列代碼都是非法的:

int& ri = 60;

list<int>& rl = list<int>();

int fun(){…}; int& rf = fun();

(to be continued!)

吳桐寫于2003.6.16

最近修改2003.6.16

auto_ptr_ref的奇妙(下)在我們前面談到的auto_ptr,它的複制操作的參數類型恰好是非常量引用。是以對于下面的情況它就不能正确處理。

auto_ptr<int> ap1 = auto_ptr<int>(new int(8));//等号右邊的是一個臨時右值

auto_ptr<int> fun()//一個生成auto_ptr<int>的source函數

{return auto_ptr<int>(new int(8))}

auto_ptr<int> ap2 ( fun() );//調用fun()生成的auto_ptr<int>是右值

而這種情況不但合法,也是很常見的,我們不能拒絕這種用法,怎麼辦?天才的C++庫設計者已經為我們想到了這一點,auto_ptr_ref的引入就是為了解決這個問題。仔細觀察最下面的auto_ptr實作代碼,我們會看到這樣幾行:

auto_ptr(auto_ptr_ref<T> rhs) throw()

: ap(rhs.yp) {

}

auto_ptr& operator= (auto_ptr_ref<T> rhs) throw() { // new

reset(rhs.yp);

return *this;

}

template<class Y>

operator auto_ptr_ref<Y>() throw() {

return auto_ptr_ref<Y>(release());

}

這就是關鍵所在了。對于一個auto_ptr右值,不可能為它調用正常的指派運算符函數和複制構造函數,舉個例子說明,對于語句

auto_ptr<int> ap1 = auto_ptr<int>(new int(8));

首先生成臨時對象右值auto_ptr<int>(new int(8)),然後使用轉型函數模闆

template<class Y> operator auto_ptr_ref<Y>() throw()

{ return auto_ptr_ref<Y>(release());};

由auto_ptr<int>(new int(8)),首先調用成員函數release(),然後由擷取的原始指針生成另外一個臨時對象右值auto_ptr_ref<int>(一個指向動态存儲區中8的指針)。這個時候我們再看一個構造函數

auto_ptr(auto_ptr_ref<T> rhs) throw()

: ap(rhs.yp) {

}

它的參數類型不是引用,采用的是傳值(by value),是以可以接受右值,這時候,調用auto_ptr_ref<int>預設生成的複制構造函數(copy constructor),用上面最後得到的那個auto_ptr_ref<int>臨時對象右值作為引數,生成了rhs,接着auto_ptr_ref<int>臨時對象右值自動析構結束生命,後面的ap(rhs.yp)完成最後的工作。

我們的整個探險過程就結束了。最好參考《The C++ Standard Library》中講解auto_ptr使用的章節一起閱讀。這樣auto_ptr的使用和所有設計原理對我們就不再神秘了。

附錄:auto_ptr的實作代碼,來自Nicolai M. Josuttis

namespace std {

// auxiliary type to enable copies and assignments (now global)

template<class Y>

struct auto_ptr_ref {

Y* yp;

auto_ptr_ref (Y* rhs)

: yp(rhs) {

}

};

template<class T>

class auto_ptr {

private:

T* ap; // refers to the actual owned object (if any)

public:

typedef T element_type;

// constructor

explicit auto_ptr (T* ptr = 0) throw()

: ap(ptr) {

}

// copy constructors (with implicit conversion)

// - note: nonconstant parameter

auto_ptr (auto_ptr& rhs) throw()

: ap(rhs.release()) {

}

template<class Y>

auto_ptr (auto_ptr<Y>& rhs) throw()

: ap(rhs.release()) {

}

// assignments (with implicit conversion)

// - note: nonconstant parameter

auto_ptr& operator= (auto_ptr& rhs) throw() {

reset(rhs.release());

return *this;

}

template<class Y>

auto_ptr& operator= (auto_ptr<Y>& rhs) throw() {

reset(rhs.release());

return *this;

}

// destructor

~auto_ptr() throw() {

delete ap;

}

// value access

T* get() const throw() {

return ap;

}

T& operator*() const throw() {

http://www.xnovo.com/doc/html/aebbbgbgbbfhaadsyjxztedd.html

auto_ptr_ref的奇妙(下)在我們前面談到的auto_ptr,它的複制操作的參數類型恰好是非常量引用。是以對于下面的情況它就不能正确處理。

auto_ptr<int> ap1 = auto_ptr<int>(new int(8));//等号右邊的是一個臨時右值

auto_ptr<int> fun()//一個生成auto_ptr<int>的source函數

{return auto_ptr<int>(new int(8))}

auto_ptr<int> ap2 ( fun() );//調用fun()生成的auto_ptr<int>是右值

而這種情況不但合法,也是很常見的,我們不能拒絕這種用法,怎麼辦?天才的C++庫設計者已經為我們想到了這一點,auto_ptr_ref的引入就是為了解決這個問題。仔細觀察最下面的auto_ptr實作代碼,我們會看到這樣幾行:

auto_ptr(auto_ptr_ref<T> rhs) throw()

: ap(rhs.yp) {

}

auto_ptr& operator= (auto_ptr_ref<T> rhs) throw() { // new

reset(rhs.yp);

return *this;

}

template<class Y>

operator auto_ptr_ref<Y>() throw() {

return auto_ptr_ref<Y>(release());

}

這就是關鍵所在了。對于一個auto_ptr右值,不可能為它調用正常的指派運算符函數和複制構造函數,舉個例子說明,對于語句

auto_ptr<int> ap1 = auto_ptr<int>(new int(8));

首先生成臨時對象右值auto_ptr<int>(new int(8)),然後使用轉型函數模闆

template<class Y> operator auto_ptr_ref<Y>() throw()

{ return auto_ptr_ref<Y>(release());};

由auto_ptr<int>(new int(8)),首先調用成員函數release(),然後由擷取的原始指針生成另外一個臨時對象右值auto_ptr_ref<int>(一個指向動态存儲區中8的指針)。這個時候我們再看一個構造函數

auto_ptr(auto_ptr_ref<T> rhs) throw()

: ap(rhs.yp) {

}

它的參數類型不是引用,采用的是傳值(by value),是以可以接受右值,這時候,調用auto_ptr_ref<int>預設生成的複制構造函數(copy constructor),用上面最後得到的那個auto_ptr_ref<int>臨時對象右值作為引數,生成了rhs,接着auto_ptr_ref<int>臨時對象右值自動析構結束生命,後面的ap(rhs.yp)完成最後的工作。

我們的整個探險過程就結束了。最好參考《The C++ Standard Library》中講解auto_ptr使用的章節一起閱讀。這樣auto_ptr的使用和所有設計原理對我們就不再神秘了。

附錄:auto_ptr的實作代碼,來自Nicolai M. Josuttis

namespace std {

// auxiliary type to enable copies and assignments (now global)

template<class Y>

struct auto_ptr_ref {

Y* yp;

auto_ptr_ref (Y* rhs)

: yp(rhs) {

}

};

template<class T>

class auto_ptr {

private:

T* ap; // refers to the actual owned object (if any)

public:

typedef T element_type;

// constructor

explicit auto_ptr (T* ptr = 0) throw()

: ap(ptr) {

}

// copy constructors (with implicit conversion)

// - note: nonconstant parameter

auto_ptr (auto_ptr& rhs) throw()

: ap(rhs.release()) {

}

template<class Y>

auto_ptr (auto_ptr<Y>& rhs) throw()

: ap(rhs.release()) {

}

// assignments (with implicit conversion)

// - note: nonconstant parameter

auto_ptr& operator= (auto_ptr& rhs) throw() {

reset(rhs.release());

return *this;

}

template<class Y>

auto_ptr& operator= (auto_ptr<Y>& rhs) throw() {

reset(rhs.release());

return *this;

}

// destructor

~auto_ptr() throw() {

delete ap;

}

// value access

T* get() const throw() {

return ap;

}

T& operator*() const throw() {

http://www.xnovo.com/doc/html/aebbbgbgbbfhaadsyjxztedd.html

繼續閱讀