三、Resource Management
所謂的資源就是:一旦用了它,將來就必須還給系統。
C++ 中最常使用的資源是動態配置記憶體,其它常見的還有:file descriptor, mutex locks, database connection, network sockets ...,不論是哪一種,重要的是,當不再使用時,必須將它還給系統。
嘗試在任何運用情況下,都要確保以上所言,是件困難的事,而本章,就是將一個直接易種且基於物件(object-oriented) 的資源管理辦法,建立在 C++ 對 constructor, destructor, copying function 的基礙上。
13、Use objects to manage resources
正常來說,函式的設計是這樣子的:
這樣子的寫法,看起來似乎妥當,然而如果在 f 中途有發生 exception 或提早 return 或類似的情況,將可能造成 delete 沒有順利執行到。
為確保 createInvestment 所傳回的資源總是會被釋放,該做的是:
將資源放進物件中,當控制流離開 f 後,該物件的 destructor 會自動釋放那些資源。
許多資源被動態置於 heap 內,而後被使用於單一區塊或函式內,它們應該在控制流離開相關區塊或函式時被釋放,標準程程式庫提供的 auto_ptr 正是針對這種形勢而設計的特製產品。
auto_ptr 是「pointer-like 物件」,也就是所謂的「智慧型指標」,它的 destructor 會自動對所指的物件呼叫 delete,用法如下:
以 createInvestment 傳回的資源,當成管理者 auto_ptr 的初值。
「以物件管理資源」的兩個關鍵想法:
獲得資源後,立刻放到管理物件(managing object)內,事實上「以物件管理資源」的觀念常被稱為「資源取得時機便是初始化時機」(Resource Acquisition Is Initialization; RAII),因為幾乎總是在獲得一筆資源後,於同一述句內以它初始化某個管理物件。
管理物件(managing object) 運用 destructor 確保資源被釋放,一旦管理物件被銷毀(例如當物件離開作用域),則其 destructor 會被自動喚起,於是資源將被釋放。
要注意的是,因為 auto_ptr 被銷毀時,會自動刪除它所指的物件,所以別讓超過一個的 auto_ptr 同時指向同一個物件。
為了預防這個問題,如果是透過 copy constuctor 或 copy assignment operator 來複製它們,則它們會變成 NULL ,所複製所得的指標將成為取得資源的唯一擁有權:
這種詭異的複製行為,再加上不同同時有超過一個的 auto_ptr 指向同一物件,意味著 auto_ptr 並非管理動態配置資源的神兵利器,例如,STL 容器要求元素發揮「正常的」複製行為,因些這些容器容不得 auto_ptr。
auto_ptr 的替代方案是「reference-counting smart pointer; RCSP」:
RCSP 也是智慧型指標,會持續追縱有多少個物件指向某比資源,並在無人指向它時,自動刪除該資源。
不過 RCSP 無法打破 cycles of references ,這是容易發生 memory leak 的地方。
TR1 的 tr1::shared_ptr 就是個 RCSP,詳情可見條款 54:
當然 tr1::shared_ptr 的複製行為「一如預期」,所以可被用於 STL 容器上。
auto_ptr 及 tr1::shared_ptr 兩者都是在 destructor 內作 delete,而不是 delete [],這兩者的差別在條款 16 有描述,所以在動態配置而得的 array 上使用 auto_ptr 或 tr1::shared_ptr,將會是個餿主意。
而 C++ 也沒有特別針對「動態配置陣列」而設計類似的 auto_ptr 及 tr1::shared_ptr,因為利用 vector 和 string 幾乎總是可以取代動態配置而得的陣列。
有時候,所使用的資源是 auto_ptr 或 tr1::shared_ptr 這些預製式 class 所無法妥善管理的,此時,就需要製作自己的資源管理類別,將在條款 14, 15 介紹。
另外 createInvestment 這個介面,會讓呼叫者極易忘記 delete 回傳的指標,所以必須對此介面進行修改,將在條款 18 說明。
為了防止 memory leak,儘量使用 RAII 物件,它會在 constructor 中獲得資源,並在 destructor 中釋放資源。
兩個常被使用的 RAII class 是:tr1::shared_ptr 及 auto_ptr。
14、Think carefully about copying behavior in resource-managing classes.
並非所有資源都是 heap-based,對於這種資源而言,像 auto_ptr 及 tr1::shared_ptr 這樣的 smart pointer 往往不適合做為資源掌管者(resource handler),所以,有可能,將需要建立自己的資源管理類別(resource-managing classes)。
假設使用 C API 函式處理型別為 Mutex 的 mutex objects,共有 lock 及 unlock 兩函式可用,而為了確保不會忘記將 Mutex 解鎖,所以會希望建立一個 class 來管理:
此時,要考慮到的情況是:如果 Lock 物件被複製,則其行為模式應該為何?
tr1::shared_ptr 在條款 13 介紹的是當 reference count 為 0 時,刪除所指的物件,不過 tr1::shared_ptr 也允許指定所謂的 deleter,也就是當成 constructor 中可有可無的第二個參數,這個 deleter 可以是一個 function 或 function object:
在這個例子中,並沒有宣告 destructor,因為 class 的 destructor 會自動喚起在 class 中的 non-static 成員變數的 destructor,不論是編譯器生成或是使用者自訂的 destructor 都是如此。
而 mutexPtr 的 destructor 會在 Mutex 的 reference count 為 0 時,自動呼叫 tr1::shared_ptr 的 deleter,在本例中也就是 unlock。
複製 RAII 物件必須一併處理它所管理的資源,而資源的 copying 行為將決定 RAII 物件的 copying 行為。
常見的 RAII class copying 行為是:抑制 copying、使用 reference counting。
15、Provide access to raw resources in resource-managing classes.
resource-managing classes 是對抗資源洩漏的保壘,而在完美的情況下,也將依賴這樣的 classes 來處理和資源之間的所有互動。
在條款 13 中,使用 smart pointer 如:auto_ptr 和 tr1:shared_ptr 來保存 factory function 的呼叫結果:
此時,如果希望以某個 function 來處理 Investment 物件,如:
但卻無法直接呼叫,因為 dayHeld 需要的是 Investment* 的 pointer,而 pInv 是 tr1::shared_ptr 的物件。
這時候就需要一個 function ,可以將 RAII class 轉換成其內含之原始資料,有兩個作法可以達成:
顯示轉換和隱式轉換。
auto_ptr 和 tr1:shared_ptr 都提供一個 get 成員函式,用來執行顯示轉換,也就是它會傳回 smart pointer 內部的原始指標(的複件):
就和(幾乎)所有的 smart pointer 一樣,auto_ptr 和 tr1:shared_ptr 也重載了指標取值(pointer dereferencing)運算子(operator-> 和 operator*),它們允許了隱式轉換至底部原始指標:
但有時候,還是需要取得 RAII 物件內的原始資源,作法同樣分為顯式和隱式,以下列這個用於字型的 RAII class 作例子:
對於「將 Font 物件轉換為 FontHandle」可能會是很頻繁的需求,Font class 可以提供一個顯示轉換函式:
如果嫌這般的顯示轉換太麻煩,另一個作法是令 Font 提供隱式轉換函式,轉型為 FontHandle:
但這個隱式轉換會增加錯誤發生的機會,例如會在需要 Font 時,意外產生一個 FontHandle:
以上程式有個 FontHandle 由 Font 物件 f1 管理,但那個 FontHandle 也可透過 f2 取得,則當 f1 被銷毀,字型被釋放時, f2 將成為 dongle。
對於該提供顯式轉換或隱式轉換,答案取決於 RAII class 被設計執行的特定工作及被使用的情況,最佳設計很可以是堅持條款 18 的忠告:「讓介面容易被正確使用」,通常顯式轉換函式是比較受歡迎的路子。
如果讓 RAII class 傳回原始資源函式,這是否與「封裝」發生矛盾了?
的確,但一般而言 RAII classes 並不是為了封裝某物而存在,只是為了確保資源釋放會發生,當然也可以在加上一層資源封裝,如果必要的話。
APIs 往往要求存取原始資源,所以每個 RAII class 都應該提供一個「取得其管理之資源」的方式。
對原始資源的存取可能經由顯示轉換或隱式轉換,一般而言,顯示轉換較為安全。
16、Use the same form in corresponding uses of new and delete.
以下動作有什麼錯?
當透過 new 動態生成一個物件,有兩件事發生:
第一,記憶體被配置出來,透過名為 operator new 的函式,詳見條款 49及51。
第二,針對此記憶體會有一個或多個 constructor 被喚起。
當使用 delete 時,也會有兩件事發生:
第一,針對此記憶體會有一個或多個 destructor 被喚起。
第二,透過名為 operator delete 的函式釋放記憶體,詳見條款51。
delete 的最大問題在於:
即將被刪除的記憶體內究竟存有多少物件?這個問題的答案決定了有多少個 destructor 必須被喚起。
事實上,這個問題可以更簡單些:
即將被刪除的那個指標,所指的是單一物件還是物件陣列?這是必不可缺的問題,因為單一物件的記憶體佈局一般而言不同於陣列的記憶體佈局。
更明確的說,陣列所用的記憶體內還通還包括「陣列大小」的記錄,單一物件的記憶體則不需要這筆記錄。
所以當對一個指標使 delete,唯一能讓 delete 知道記憶體中是否存在一個「陣列大小記錄」的辦法就是:
在 delete 時,加上中括號,否則 delete 便認定指標指向單一物件:
考慮下面這個 typedef 的用法:
為了避免諸如此類的錯誤,儘量不要對陣列形式作 typedef 動作。
如果在 new 算式中使用 [],必須也在相應的 delete 算式中使用 [],如果在 new 算式中不使用 [],一定也不要在相應的 delete 算式中使用 []。
17、Store newed objects in smart pointers in standalone statements.
假設有個函式用來揭示處理程序的優先權,另一個函式用來在某個動態配置所得的 Widget 上進行某些帶有優先權的處理:
令人驚訝的是,這樣的寫法可能會導致洩漏資源。
在呼叫 processWidget 之後,編譯器必須產生程式碼,完成三件事:
Java 和 C# 總是以特定次序完成函式參數的核算,但 C++ 不同,唯一可以確定的是 "new Widget" 一定執行在呼叫 std::tr1::shared_ptr constructor 之前,所以如果編譯器的操作序列如下:
則萬一 priority() 的呼叫導致 exception,則 "new Widget" 傳回的指標將會遺失,而造成資源洩漏。
為了要避免類似的問題,要使用分離句:
之所以行得通,是因為 C++ 編譯器對於「跨越述句的各項操作」沒有重新排列的自由,所以編譯器無法更動它們之間的執行次序。
以獨立述句將 newed 物件儲存於 smart pointer 內,以避免 exception 被拋出,而造成資源洩漏。
所謂的資源就是:一旦用了它,將來就必須還給系統。
C++ 中最常使用的資源是動態配置記憶體,其它常見的還有:file descriptor, mutex locks, database connection, network sockets ...,不論是哪一種,重要的是,當不再使用時,必須將它還給系統。
嘗試在任何運用情況下,都要確保以上所言,是件困難的事,而本章,就是將一個直接易種且基於物件(object-oriented) 的資源管理辦法,建立在 C++ 對 constructor, destructor, copying function 的基礙上。
13、Use objects to manage resources
正常來說,函式的設計是這樣子的:
class Investment { ... }; // base class
Investment* createInvestment(); // factory function
void f() {
Investment* pInv = createInvestment(); // 呼叫 factory function
...
delete pInv; // 釋放 pInv 所指物件
}
這樣子的寫法,看起來似乎妥當,然而如果在 f 中途有發生 exception 或提早 return 或類似的情況,將可能造成 delete 沒有順利執行到。
為確保 createInvestment 所傳回的資源總是會被釋放,該做的是:
將資源放進物件中,當控制流離開 f 後,該物件的 destructor 會自動釋放那些資源。
許多資源被動態置於 heap 內,而後被使用於單一區塊或函式內,它們應該在控制流離開相關區塊或函式時被釋放,標準程程式庫提供的 auto_ptr 正是針對這種形勢而設計的特製產品。
auto_ptr 是「pointer-like 物件」,也就是所謂的「智慧型指標」,它的 destructor 會自動對所指的物件呼叫 delete,用法如下:
void f() {
std::auto_ptr pInv(createInvestment());
// 呼叫 factory function
...
// 經由 auto_ptr 的 destructor 自動 delete pInv
}
以 createInvestment 傳回的資源,當成管理者 auto_ptr 的初值。
「以物件管理資源」的兩個關鍵想法:
獲得資源後,立刻放到管理物件(managing object)內,事實上「以物件管理資源」的觀念常被稱為「資源取得時機便是初始化時機」(Resource Acquisition Is Initialization; RAII),因為幾乎總是在獲得一筆資源後,於同一述句內以它初始化某個管理物件。
管理物件(managing object) 運用 destructor 確保資源被釋放,一旦管理物件被銷毀(例如當物件離開作用域),則其 destructor 會被自動喚起,於是資源將被釋放。
要注意的是,因為 auto_ptr 被銷毀時,會自動刪除它所指的物件,所以別讓超過一個的 auto_ptr 同時指向同一個物件。
為了預防這個問題,如果是透過 copy constuctor 或 copy assignment operator 來複製它們,則它們會變成 NULL ,所複製所得的指標將成為取得資源的唯一擁有權:
std::auto_ptrpInv(createInvestment()); std::auto_ptr // pInv1 指向物件,pInv2 被設為 NULLpInv2(pInv1); // pInv2 指向物件,pInv1 被設為 NULL pInv1 = pInv2;
這種詭異的複製行為,再加上不同同時有超過一個的 auto_ptr 指向同一物件,意味著 auto_ptr 並非管理動態配置資源的神兵利器,例如,STL 容器要求元素發揮「正常的」複製行為,因些這些容器容不得 auto_ptr。
auto_ptr 的替代方案是「reference-counting smart pointer; RCSP」:
RCSP 也是智慧型指標,會持續追縱有多少個物件指向某比資源,並在無人指向它時,自動刪除該資源。
不過 RCSP 無法打破 cycles of references ,這是容易發生 memory leak 的地方。
TR1 的 tr1::shared_ptr 就是個 RCSP,詳情可見條款 54:
void f() {
std::tr1::shared_ptr pInv(createInvestment());
// 呼叫 factory function
...
// 經由 auto_ptr 的 destructor 自動 delete pInv
}
當然 tr1::shared_ptr 的複製行為「一如預期」,所以可被用於 STL 容器上。
auto_ptr 及 tr1::shared_ptr 兩者都是在 destructor 內作 delete,而不是 delete [],這兩者的差別在條款 16 有描述,所以在動態配置而得的 array 上使用 auto_ptr 或 tr1::shared_ptr,將會是個餿主意。
而 C++ 也沒有特別針對「動態配置陣列」而設計類似的 auto_ptr 及 tr1::shared_ptr,因為利用 vector 和 string 幾乎總是可以取代動態配置而得的陣列。
有時候,所使用的資源是 auto_ptr 或 tr1::shared_ptr 這些預製式 class 所無法妥善管理的,此時,就需要製作自己的資源管理類別,將在條款 14, 15 介紹。
另外 createInvestment 這個介面,會讓呼叫者極易忘記 delete 回傳的指標,所以必須對此介面進行修改,將在條款 18 說明。
為了防止 memory leak,儘量使用 RAII 物件,它會在 constructor 中獲得資源,並在 destructor 中釋放資源。
兩個常被使用的 RAII class 是:tr1::shared_ptr 及 auto_ptr。
14、Think carefully about copying behavior in resource-managing classes.
並非所有資源都是 heap-based,對於這種資源而言,像 auto_ptr 及 tr1::shared_ptr 這樣的 smart pointer 往往不適合做為資源掌管者(resource handler),所以,有可能,將需要建立自己的資源管理類別(resource-managing classes)。
假設使用 C API 函式處理型別為 Mutex 的 mutex objects,共有 lock 及 unlock 兩函式可用,而為了確保不會忘記將 Mutex 解鎖,所以會希望建立一個 class 來管理:
class Lock {
public:
explicit Lock(Mutex* pm) : mutexPtr(pm) {
lock(mutexPtr); // 獲得資源
}
~Lock() { unlock(mutexPtr); } // 釋放資源
private:
Mutex* mutexPtr;
};
此時,要考慮到的情況是:如果 Lock 物件被複製,則其行為模式應該為何?
Lock ml1(&m); // 鎖定 m Lock ml2(ml1); // 將 ml1 複製到 ml2
- 通常以前兩種較為常用:
- 禁止複製:許多時候,允許 RAII 物件被複製並不合理,此時可以參考條款 6,有說明該如何禁止複製。
- 對底層資源採用「reference-count」:有時候希望可以保有資源,直到它的最後一個使用者被銷毀,這種情況下複製 RAII 物件時,應該將資源的「被引用數」遞增,類似 tr1::shared_ptr 的作法。
- 複製底部資源:有些情況,可以針對一份資源擁有任意數量的複製,而「資源管理類別」的唯一理由是,當不再需要某個複件時,確保它被釋放。這種情況下,在複製資源管理物件時,應該也要同時複製其所包覆的資源。
- 轉移底部資源擁有權:在某些罕見的場合,可能希望確保永遠只有一個 RAII 物件指向一個 raw resource,即使 RAII 被複製,此時資源的擁有權會從被複製物移到標的物,如何條款 13 所描述的 auto_ptr 的複製意義。
tr1::shared_ptr 在條款 13 介紹的是當 reference count 為 0 時,刪除所指的物件,不過 tr1::shared_ptr 也允許指定所謂的 deleter,也就是當成 constructor 中可有可無的第二個參數,這個 deleter 可以是一個 function 或 function object:
class Lock {
public:
explicit Lock(Mutex* pm) : mutexPtr(pm, unlock) {
// 以某個 Mutex 初始化 shared_ptr,並以 unlock 為 deleter
lock(mutexPtr.get()); // 條款 15 會談到 get
}
private:
std::tr1::shared_ptr mutexPtr;
};
在這個例子中,並沒有宣告 destructor,因為 class 的 destructor 會自動喚起在 class 中的 non-static 成員變數的 destructor,不論是編譯器生成或是使用者自訂的 destructor 都是如此。
而 mutexPtr 的 destructor 會在 Mutex 的 reference count 為 0 時,自動呼叫 tr1::shared_ptr 的 deleter,在本例中也就是 unlock。
複製 RAII 物件必須一併處理它所管理的資源,而資源的 copying 行為將決定 RAII 物件的 copying 行為。
常見的 RAII class copying 行為是:抑制 copying、使用 reference counting。
15、Provide access to raw resources in resource-managing classes.
resource-managing classes 是對抗資源洩漏的保壘,而在完美的情況下,也將依賴這樣的 classes 來處理和資源之間的所有互動。
在條款 13 中,使用 smart pointer 如:auto_ptr 和 tr1:shared_ptr 來保存 factory function 的呼叫結果:
std::tr1::shared_ptr pInv(createInvestment());
此時,如果希望以某個 function 來處理 Investment 物件,如:
int dayHeld(const Investment* pi); // 傳回投資天數
但卻無法直接呼叫,因為 dayHeld 需要的是 Investment* 的 pointer,而 pInv 是 tr1::shared_ptr
這時候就需要一個 function ,可以將 RAII class 轉換成其內含之原始資料,有兩個作法可以達成:
顯示轉換和隱式轉換。
auto_ptr 和 tr1:shared_ptr 都提供一個 get 成員函式,用來執行顯示轉換,也就是它會傳回 smart pointer 內部的原始指標(的複件):
int days = daysHeld(pInv); // 錯誤,無法通過編譯 int days = daysHeld(pInv.get()); // 將 Pinv 內的原始指標傳出
就和(幾乎)所有的 smart pointer 一樣,auto_ptr 和 tr1:shared_ptr 也重載了指標取值(pointer dereferencing)運算子(operator-> 和 operator*),它們允許了隱式轉換至底部原始指標:
class Investment {
public:
bool isTaxFree() const;
...
};
Investment* createInvestment(); // factory function
std::tr1::shared_ptr pi1(createInvestment());
bool taxable1 = !(pi1->isTaxFree()); // 經由 operator-> 存取資源
...
std::auto_ptr pi2(createInvestment());
bool taxable2 = !((*pi2).isTaxFree()); // 經由 operator* 存取資源
...
但有時候,還是需要取得 RAII 物件內的原始資源,作法同樣分為顯式和隱式,以下列這個用於字型的 RAII class 作例子:
FontHandle getFont();
void releaseFont(FontHandle fh);
class Font {
public:
explicit Font(FontHandle fh) : f(fh) { }
~Font() { releaseFont(f); }
private:
FontHandle f;
};
對於「將 Font 物件轉換為 FontHandle」可能會是很頻繁的需求,Font class 可以提供一個顯示轉換函式:
class Font {
public:
...
FontHandle get() const { return f;} // 顯式轉換函式
...
};
void changeFontSize(FontHandle f, int newSize);
Font f(getFont());
int newFontSize;
changeFontSize(f.get(), new FontSize);
// 明白地將 Font 轉換為 FontHandle
如果嫌這般的顯示轉換太麻煩,另一個作法是令 Font 提供隱式轉換函式,轉型為 FontHandle:
class Font {
public:
...
operator FontHandle () const { return f; } // 隱式轉換函式
...
};
Font f(getFont());
int newFontSize;
changeFontSize(f, new FontSize);
// 將 Font 隱式轉換為 FontHandle
但這個隱式轉換會增加錯誤發生的機會,例如會在需要 Font 時,意外產生一個 FontHandle:
Font f1(getFont());
...
FontHandle f2 = f1; // 原意是要拷貝一個 Font 物件
// 卻反而將 f1 隱式轉換成其底部的 FontHandle 並複製給 f2
以上程式有個 FontHandle 由 Font 物件 f1 管理,但那個 FontHandle 也可透過 f2 取得,則當 f1 被銷毀,字型被釋放時, f2 將成為 dongle。
對於該提供顯式轉換或隱式轉換,答案取決於 RAII class 被設計執行的特定工作及被使用的情況,最佳設計很可以是堅持條款 18 的忠告:「讓介面容易被正確使用」,通常顯式轉換函式是比較受歡迎的路子。
如果讓 RAII class 傳回原始資源函式,這是否與「封裝」發生矛盾了?
的確,但一般而言 RAII classes 並不是為了封裝某物而存在,只是為了確保資源釋放會發生,當然也可以在加上一層資源封裝,如果必要的話。
APIs 往往要求存取原始資源,所以每個 RAII class 都應該提供一個「取得其管理之資源」的方式。
對原始資源的存取可能經由顯示轉換或隱式轉換,一般而言,顯示轉換較為安全。
16、Use the same form in corresponding uses of new and delete.
以下動作有什麼錯?
std::string* stringArray = new std::string[100];
...
delete stringArray;
當透過 new 動態生成一個物件,有兩件事發生:
第一,記憶體被配置出來,透過名為 operator new 的函式,詳見條款 49及51。
第二,針對此記憶體會有一個或多個 constructor 被喚起。
當使用 delete 時,也會有兩件事發生:
第一,針對此記憶體會有一個或多個 destructor 被喚起。
第二,透過名為 operator delete 的函式釋放記憶體,詳見條款51。
delete 的最大問題在於:
即將被刪除的記憶體內究竟存有多少物件?這個問題的答案決定了有多少個 destructor 必須被喚起。
事實上,這個問題可以更簡單些:
即將被刪除的那個指標,所指的是單一物件還是物件陣列?這是必不可缺的問題,因為單一物件的記憶體佈局一般而言不同於陣列的記憶體佈局。
更明確的說,陣列所用的記憶體內還通還包括「陣列大小」的記錄,單一物件的記憶體則不需要這筆記錄。
所以當對一個指標使 delete,唯一能讓 delete 知道記憶體中是否存在一個「陣列大小記錄」的辦法就是:
在 delete 時,加上中括號,否則 delete 便認定指標指向單一物件:
std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];
...
delete stringPtr1; // 刪除一個物件
delete [] stringPtr2; // 刪除一個物件陣列
考慮下面這個 typedef 的用法:
typedef std::string AddressLines[4];
// 地址有4行,每行都是一個 string
std::string* pa1 = new AddressLines;
// new AddressLines 傳回一個 string* 就像 new string[4] 一樣
...
delete pal; // 錯誤,行為未有定義
delete [] pal; // 正確
為了避免諸如此類的錯誤,儘量不要對陣列形式作 typedef 動作。
如果在 new 算式中使用 [],必須也在相應的 delete 算式中使用 [],如果在 new 算式中不使用 [],一定也不要在相應的 delete 算式中使用 []。
17、Store newed objects in smart pointers in standalone statements.
假設有個函式用來揭示處理程序的優先權,另一個函式用來在某個動態配置所得的 Widget 上進行某些帶有優先權的處理:
int priority();
void processWidget(std::tr1::shared_ptr pw, priority());
接著考慮呼叫 processWidget:
processWidget(new Widget, priority);
// 無法通過編譯,std::tr1::shared_ptr 的 constructor 為 explicit
processWidget(std::tr1::shared_ptr(new Widget), priority);
// 可以通過編譯
令人驚訝的是,這樣的寫法可能會導致洩漏資源。
在呼叫 processWidget 之後,編譯器必須產生程式碼,完成三件事:
- 呼叫 priority()
- 執行 "new Widget"
- 呼叫 std::tr1::shared_ptr constructor
Java 和 C# 總是以特定次序完成函式參數的核算,但 C++ 不同,唯一可以確定的是 "new Widget" 一定執行在呼叫 std::tr1::shared_ptr constructor 之前,所以如果編譯器的操作序列如下:
- 執行 "new Widget"
- 呼叫 priority()
- 呼叫 std::tr1::shared_ptr constructor
則萬一 priority() 的呼叫導致 exception,則 "new Widget" 傳回的指標將會遺失,而造成資源洩漏。
為了要避免類似的問題,要使用分離句:
std::tr1::shared_ptr pw(new Widget);
processWidget(pw, priority());
之所以行得通,是因為 C++ 編譯器對於「跨越述句的各項操作」沒有重新排列的自由,所以編譯器無法更動它們之間的執行次序。
以獨立述句將 newed 物件儲存於 smart pointer 內,以避免 exception 被拋出,而造成資源洩漏。
全站熱搜