C++ 習(xí)慣RAII思想
什么是 RAII
RAII(資源獲取即初始化,Resource Acquisition Is Initialization),作為 C++ 的一個(gè)重要編程范式,已經(jīng)被貫徹于標(biāo)準(zhǔn)庫(kù)的各個(gè)角落。RAII 的核心思想是將資源與類(lèi)的生命周期綁定,RAII 類(lèi)是針對(duì)內(nèi)部資源封裝的資源管理類(lèi)。
RAII 有什么作用
RAII 的作用主要體現(xiàn)在:自動(dòng)資源管理,異常安全,簡(jiǎn)化代碼,提高可維護(hù)性。
自動(dòng)資源管理 獲取資源后交由 RAII 類(lèi)保管,離開(kāi)作用域后資源被妥善釋放,減少手動(dòng)資源管理容易出現(xiàn)的忘記釋放和重復(fù)釋放。
異常安全 代碼可能在任何步驟拋出異常,C++ 保證在異常發(fā)生后,已經(jīng)完全構(gòu)造的局部變量會(huì)被析構(gòu),所以如果資源被一個(gè)已經(jīng)構(gòu)造好的 RAII 類(lèi)保存著,那么在異常發(fā)生后它就能被安全釋放。
簡(jiǎn)化代碼 在復(fù)雜邏輯,特別是多返回路徑的函數(shù)中,使用 RAII 類(lèi)管理資源或狀態(tài),可大大降低手動(dòng)管理帶來(lái)的復(fù)雜性,增強(qiáng)可讀性。
提高可維護(hù)性 RAII 類(lèi)封裝了資源管理的細(xì)節(jié),與其他邏輯分離,便于代碼維護(hù)。
RAII 類(lèi)的工作原理
RAII 類(lèi)依賴(lài)于 C++ 的棧對(duì)象生命周期管理機(jī)制,通過(guò)定義構(gòu)造、拷貝和析構(gòu)函數(shù)來(lái)精確控制類(lèi)在創(chuàng)建、復(fù)制和銷(xiāo)毀時(shí)的行為,以實(shí)現(xiàn)核心的資源保存、流轉(zhuǎn)和釋放。
構(gòu)造函數(shù) 構(gòu)造函數(shù)接受資源,將其存儲(chǔ)在類(lèi)中,同時(shí)初始化相關(guān)狀態(tài)或接受其他與資源管理相關(guān)聯(lián)的數(shù)據(jù)。比如 std::shared_ptr 除了存儲(chǔ)指針外,還存儲(chǔ)該指針的引用計(jì)數(shù),在構(gòu)造時(shí)必須初始化引用計(jì)數(shù),它還支持傳入自定義的刪除器(我的上一篇隨筆C++ 智能指針的刪除器對(duì)它作過(guò)討論)。
拷貝和移動(dòng)函數(shù) 包括拷貝構(gòu)造、移動(dòng)構(gòu)造、拷貝賦值、移動(dòng)賦值四個(gè)成員函數(shù),它們共同描述了資源的轉(zhuǎn)移行為。
當(dāng)資源為獨(dú)占時(shí),就不能允許發(fā)生復(fù)制動(dòng)作,那么拷貝構(gòu)造和拷貝賦值函數(shù)應(yīng)該定義為刪除,但是從一個(gè)臨時(shí)的 RAII 類(lèi)接管資源很合理,所以需要定義它的移動(dòng)構(gòu)造和移動(dòng)賦值函數(shù)。一個(gè)現(xiàn)成的例子就是 std::unique_ptr:
代碼
std::unique_ptr<int> create_unique(int value)
{
std::unique_ptr<int> ret(new int(value));
return ret;//可能觸發(fā){{tip-code}}NRVO{{/tip-code}}
}
std::unique_ptr<int> piu1(new int(42));
std::unique_ptr<int> piu2 = piu1;//錯(cuò)誤,無(wú)法拷貝構(gòu)造
std::unique_ptr<int> piu3;
piu3 = piu1;//錯(cuò)誤,無(wú)法拷貝賦值
piu3 = create_unique(42);//可以,接管指針
piu3 = std::move(piu1);//強(qiáng)行轉(zhuǎn)移所有權(quán)
piu3.reset(piu1.release());//使用unique_ptr提供的接口強(qiáng)行轉(zhuǎn)移所有權(quán)
上述代碼提到 NRVO(Named Return Value Optimization,具名返回值優(yōu)化)是 C++ 拷貝消除機(jī)制(Copy Elision)的一種具體形式,該機(jī)制旨在消除不必要的臨時(shí)對(duì)象拷貝以提高程序性能,可到 cppreference:copy_elision 查看詳細(xì)講解。
示例代碼中的 create_unique 返回一個(gè)名為 ret 的局部變量,并且沒(méi)有其他引用綁定到 ret 上,如果這樣調(diào)用create_unique:std::unique_ptr<int> piu4 = create_unique(42); 在編譯器支持 NRVO 的情況下,ret 變量不會(huì)被實(shí)際創(chuàng)建,而是直接在外部 piu4 的內(nèi)存位置直接構(gòu)造,達(dá)到消除拷貝的目的。
若編譯器未支持或者代碼情況不滿(mǎn)足 NRVO 條件,移動(dòng)構(gòu)造則作為第二候選用來(lái)避免拷貝,拷貝構(gòu)造的優(yōu)先級(jí)最低,因?yàn)榭截愐粋€(gè)對(duì)象可能付出高昂的代價(jià)。
由于 std::unique_ptr 刪除了拷貝構(gòu)造和拷貝賦值函數(shù),我們無(wú)法復(fù)制一個(gè)現(xiàn)有的實(shí)例;但是定義了移動(dòng)構(gòu)造和移動(dòng)賦值函數(shù),我們可以在函數(shù)中返回一個(gè)局部構(gòu)造的實(shí)例,用以構(gòu)造或者賦值給另一個(gè) std::unique_ptr。強(qiáng)行轉(zhuǎn)移 std::unique_ptr 的資源所有權(quán)是可以的,但是為了宣示獨(dú)占性,手動(dòng)轉(zhuǎn)移的語(yǔ)法都不那么自然。
而當(dāng)資源能夠共享時(shí),除了定義移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值函數(shù)用以接管臨時(shí)對(duì)象資源外,拷貝構(gòu)造和拷貝賦值函數(shù)的定義顯得更為重要。std::shared_ptr 的拷貝函數(shù)維護(hù)引用計(jì)數(shù),這是它實(shí)現(xiàn)指針管理的重要一環(huán);而容器類(lèi)如 std::vector 的拷貝函數(shù),需要負(fù)責(zé)可能的內(nèi)存清理和分配,所有元素的拷貝,以及過(guò)程中異常的處理。
析構(gòu)函數(shù) 析構(gòu)函數(shù)負(fù)責(zé)資源的清理工作,意味著一個(gè)實(shí)例工作的結(jié)束,但是要避免讓異常逃離析構(gòu)函數(shù)(Scott Meyers, Effective C++, Item 8)。
上述函數(shù)的主要職責(zé)是確保 RAII 類(lèi)與編譯器的協(xié)作,實(shí)現(xiàn)資源的自動(dòng)生命周期管理,而為了使資源管理更加靈活,RAII 類(lèi)通常還會(huì)提供一系列面向用戶(hù)的接口,這些接口依據(jù)具體資源的特性設(shè)計(jì),用以支持資源的讀取、修改或狀態(tài)查詢(xún),兼顧自動(dòng)化與可操作性。這使得 RAII 類(lèi)成為底層機(jī)制與上層接口之間的橋梁,保證精細(xì)復(fù)雜的資源操作以穩(wěn)定可靠的方式進(jìn)行。
使用標(biāo)準(zhǔn)庫(kù)的 RAII 設(shè)施
標(biāo)準(zhǔn)庫(kù)提供的諸多常用設(shè)施都是典型的 RAII 思想踐行者,且都是精工細(xì)作,歷經(jīng)千錘百煉的,使用它們可以使絕大部分的資源管理變得自然而簡(jiǎn)潔。
容器類(lèi) 大部分標(biāo)準(zhǔn)庫(kù)容器都需要申請(qǐng)和釋放動(dòng)態(tài)內(nèi)存,而這些工作都被標(biāo)準(zhǔn)庫(kù)的實(shí)現(xiàn)者隱藏于表面之下,阻隔了手動(dòng)管理動(dòng)態(tài)內(nèi)存的危險(xiǎn):
代碼
std::vector<int> veci;
veci.reserve(1);//預(yù)先申請(qǐng)動(dòng)態(tài)內(nèi)存
veci.push_back(0);
veci.push_back(1);//內(nèi)存不夠用了,自動(dòng)重新申請(qǐng)動(dòng)態(tài)內(nèi)存并遷移數(shù)據(jù)
veci.clear();//清理數(shù)據(jù)和內(nèi)存
文件流類(lèi) 使用標(biāo)準(zhǔn)庫(kù)的文件流類(lèi),而不是直接使用 FILE *, 那么就不用擔(dān)心有個(gè)打開(kāi)的文件在不使用之后忘記關(guān)閉了。
鎖管理類(lèi) 使用標(biāo)準(zhǔn)庫(kù)的鎖管理類(lèi)在進(jìn)入臨界區(qū)時(shí)鎖定互斥量,那么一個(gè)提前離開(kāi)臨界區(qū)的動(dòng)作就不會(huì)導(dǎo)致互斥量的解鎖被跳過(guò)了:
代碼
static std::mutex mutex1, mutex2, mutex3;
void syncOperation()
{
//C++17之前,先同時(shí)鎖定三個(gè)互斥量,然后用lock_guard領(lǐng)養(yǎng)它們
std::lock(mutex1, mutex2, mutex3);
std::lock_guard<std::mutex> guard1(mutex1, std::adopt_lock);
std::lock_guard<std::mutex> guard2(mutex2, std::adopt_lock);
std::lock_guard<std::mutex> guard3(mutex3, std::adopt_lock);
//不好的鎖定方式,若其他線程以不同的順序鎖定互斥量,極易造成死鎖
//std::lock_guard<std::mutex> guard1(mutex1);
//std::lock_guard<std::mutex> guard2(mutex2);
//std::lock_guard<std::mutex> guard3(mutex3);
//C++17之后,使用scoped_lock
std::scoped_lock lock(mutex1, mutex2, mutex3);
...//后續(xù)操作無(wú)論無(wú)論在何處返回,或者拋出異常,三個(gè)互斥量都保證能被解鎖
}
智能指針類(lèi) 使用標(biāo)準(zhǔn)庫(kù)的智能指針管理指針,那么當(dāng)無(wú)人引用該指針后,它所指涉的資源就能被及時(shí)釋放:
代碼
std::shared_ptr<int> create_shared(int value)
{
std::shared_ptr<int> ret(new int(value));
return ret;
}
//函數(shù)內(nèi)創(chuàng)建的指針被智能指針接管
auto pis1 = create_shared(42);
//資源在智能指針之間流轉(zhuǎn)
auto pis2 = pis1;
pis1.release();
std::shared_ptr<int> pis3(pis2);
...
//最后一個(gè)持有資源的智能指針析構(gòu)時(shí)釋放資源
創(chuàng)建自己的 RAII 類(lèi)
標(biāo)準(zhǔn)庫(kù)的 RAII 設(shè)施兼顧通用性和高性能,在設(shè)計(jì)上都極端考究,并且已經(jīng)可以滿(mǎn)足絕大部分的日常需求了,我們通常沒(méi)有必要去構(gòu)建與標(biāo)準(zhǔn)庫(kù)類(lèi)似的復(fù)雜設(shè)施(如果你有,那么能讀到這里我實(shí)在受寵若驚),但是將 RAII 思想應(yīng)用到日常的編碼中,也能給我們帶來(lái)諸多益處,在此我拋出幾塊拙劣的磚用以舉例。
值同步
假如我們?cè)谡{(diào)試一個(gè)函數(shù)時(shí),需要將某個(gè)值改變?yōu)橐粋€(gè)臨時(shí)的測(cè)試值,但是函數(shù)結(jié)束后,這個(gè)值需要被還原為它初始的值,不能影響后續(xù)的程序執(zhí)行:
代碼
template<typename Op, typename Tar = Op>
class ValueSynchronizer
{
public:
ValueSynchronizer(Op &operand, Tar target)
: _operand(operand), _target(target){ }
~ValueSynchronizer(){ _operand = _target; }
private:
_Op &_operand;
const Tar _target;
};
//在調(diào)試時(shí)使用(假如debug_value是一個(gè)全局變量或foo所屬類(lèi)的一個(gè)成員變量)
void foo()
{
//創(chuàng)建debug_value的一個(gè)快照
ValueSynchronizer<int> vs(debug_value, debug_value);
//后續(xù)的調(diào)試操作修改debug_value
}
現(xiàn)在無(wú)論 foo() 的邏輯多么復(fù)雜,在它返回時(shí) debug_value 一定會(huì)還原到函數(shù)進(jìn)入時(shí)的數(shù)值。
ValueSynchronizer 的設(shè)計(jì)還能讓它做其他一些事情,比如有一個(gè)設(shè)置值的函數(shù),它需要將目標(biāo)變量設(shè)置為傳入的新值,但是在離開(kāi)函數(shù)之前,舊值可能還會(huì)被使用,那么我們可以這樣編寫(xiě)這個(gè)函數(shù):
代碼
//假如_value是一個(gè)全局變量或者set_value所屬類(lèi)的一個(gè)成員變量
void set_value(int new_value)
{
ValueSynchronizer<int> vs(_value, new_value);
//其他的一些可能還會(huì)用到_value舊值的邏輯
if(_value == 0)
return;
...
}
ValueSynchronizer 的作用可以概括為:在創(chuàng)建時(shí)為操作對(duì)象指定一個(gè)目標(biāo)值,保證在離開(kāi)作用域后,該操作對(duì)象同步到設(shè)置的目標(biāo)值。
過(guò)程計(jì)時(shí)器
RAII 類(lèi)將資源與類(lèi)的生命周期綁定的特性,很容易讓人聯(lián)想到一種過(guò)程計(jì)時(shí)器的實(shí)現(xiàn):
代碼
class ScopedTimer
{
public:
explicit ScopedTimer(const std::string &scope_name)
: _start(clock()), _scope_name(scope_name){ }
~ScopedTimer()
{
std::cout<< _scope_name << " duration: "
<< (clock() - _start)/(float)CLOCKS_PER_SEC << " seconds.\n";
}
private:
clock_t _start;
const std::string _scope_name;
};
//使用過(guò)程計(jì)時(shí)器
void foo()
{
ScopedTimer function_timer(__func__);
{
ScopedTimer block_timer("inner block");
...
}
...
}
這非常適用于函數(shù)調(diào)用或者一個(gè)代碼塊的耗時(shí)統(tǒng)計(jì),不用在作用域的開(kāi)始和結(jié)束位置分別插入時(shí)間統(tǒng)計(jì)的代碼,有利于維持代碼的整潔可讀。
臨時(shí)目錄管理
數(shù)據(jù)處理類(lèi)代碼對(duì)臨時(shí)目錄的管理也非常契合 RAII 思想,在開(kāi)始處理前創(chuàng)建臨時(shí)目錄,處理過(guò)程中寫(xiě)入臨時(shí)數(shù)據(jù),過(guò)程結(jié)束后需要?jiǎng)h除臨時(shí)目錄:
代碼
class TemporaryDirectory
{
public:
explicit TemporaryDirectory(const std::string &dir): _dir(dir)
{
create_directory(_dir);
}
~TemporaryDirectory()
{
//目錄存在則將其刪除
if(directory_exist(_dir) == true)
remove_directory(_dir);
}
bool valid() const
{
return directory_exist(_dir);
}
private:
std::string _dir;
};
void process_data()
{
TemporaryDirectory td(temp);
if(td.valid() == false)
{
std::cerr<< "can't create temporary directory, abort.\n"
return;
}
...//處理數(shù)據(jù)
}
目前為止,前面理論部分強(qiáng)調(diào)的拷貝和移動(dòng)函數(shù)我們都沒(méi)有關(guān)注過(guò),因?yàn)檫@些例子使用場(chǎng)景非常簡(jiǎn)單,暫不觸及它們。如果代碼中需要它們(無(wú)論是直接的還是間接的),而我們又沒(méi)有定義時(shí),編譯器就會(huì)按需合成他們的默認(rèn)版本,具體的合成規(guī)則會(huì)深遠(yuǎn)影響到代碼的行為。
我們擴(kuò)展一下 TemporaryDirectory 的使用場(chǎng)景,以說(shuō)明這一影響。假如我們的函數(shù)接受一個(gè)臨時(shí)目錄列表,需要?jiǎng)?chuàng)建好這些臨時(shí)目錄后再開(kāi)展工作,我們這樣編寫(xiě)代碼:
代碼
void process_data(const std::vector<std::string> &dir_list)
{
//根據(jù)列表創(chuàng)建臨時(shí)目錄
std::vector<TemporaryDirectory> vectd;
for(const auto &dir : dir_list)
vectd.push_back(TemporaryDirectory(dir));
//檢查臨時(shí)目錄是否都已創(chuàng)建
for(const auto &td : vectd)
{
if(td.valid() == false)
return;
}
//永遠(yuǎn)不會(huì)到達(dá)
}
如果像示例一樣使用 TemporaryDirectory,那么可以確定,這個(gè)函數(shù)永遠(yuǎn)會(huì)在第二個(gè)檢查循環(huán)內(nèi)返回。直接原因是編譯器只為 TemporaryDirectory 創(chuàng)建了默認(rèn)的拷貝構(gòu)造函數(shù),而沒(méi)有創(chuàng)建默認(rèn)的移動(dòng)構(gòu)造函數(shù)。vectd 的 push_back 雖然接受了一個(gè)臨時(shí)的 TemporaryDirectory 對(duì)象,但是只能復(fù)制而非移動(dòng)它,臨時(shí)對(duì)象持有的 _dir 相應(yīng)的也未被移動(dòng),當(dāng) push_back 結(jié)束,這個(gè)臨時(shí)對(duì)象即被銷(xiāo)毀,創(chuàng)建的目錄也立即會(huì)被刪除。
既然問(wèn)題出在臨時(shí)對(duì)象析構(gòu),那么我們不創(chuàng)建臨時(shí)對(duì)象,直接原位構(gòu)造行不行呢:
代碼
//根據(jù)列表創(chuàng)建臨時(shí)目錄
std::vector<TemporaryDirectory> vectd;
for(const auto &dir : dir_list)
vectd.emplace_back(dir);
可惜還是不行,現(xiàn)在這個(gè)循環(huán)執(zhí)行完之后,如果 dir_list 不只有 1 個(gè)元素,大概率只有靠后的一個(gè)或幾個(gè)目錄存在。現(xiàn)在問(wèn)題牽涉 std::vector 的擴(kuò)容機(jī)制了,我們都知道,vector 在調(diào)用 emplace_back 時(shí)如果容量不夠用了,會(huì)重新開(kāi)辟內(nèi)存,將原來(lái)內(nèi)存位置的元素遷移到新內(nèi)存位置,在末尾構(gòu)造新元素,并且銷(xiāo)毀原有內(nèi)存位置所有的元素并歸還原有內(nèi)存。前面說(shuō)過(guò) TemporaryDirectory 沒(méi)有合成的移動(dòng)構(gòu)造函數(shù),所以這個(gè)遷移過(guò)程只能動(dòng)用合成的拷貝構(gòu)造函數(shù)將原來(lái)位置的 TemporaryDirectory 逐個(gè)復(fù)制而非移動(dòng)到新的位置,原來(lái)的元素持有的 _dir 相應(yīng)也未被移動(dòng),當(dāng) emplace_back 結(jié)束,這些元素都已被銷(xiāo)毀,創(chuàng)建的目錄也隨之被刪除。
上面強(qiáng)調(diào)的大概率,是因?yàn)?C++ 語(yǔ)言標(biāo)準(zhǔn)只規(guī)定了 std::vector 要保證 push_back,emplace_back 這種在尾部添加元素的操作具有攤銷(xiāo)常數(shù)時(shí)間(amortized constant time),這就導(dǎo)致它的擴(kuò)容必須采用指數(shù)增長(zhǎng)策略,即每次重新開(kāi)辟的容量是上一次的 K(K > 1.0)倍。
雖然如此,首次擴(kuò)容(容量為 0 時(shí)添加元素)分配的容量以及后續(xù)擴(kuò)容的 K 值并沒(méi)有固定的規(guī)約,每個(gè)版本的標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn)都可能不同。假如有一份標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn)首次擴(kuò)容分配 2,K 為 1.5,那么若 dir_list 內(nèi)有 2 個(gè)元素,程序會(huì)正常工作,有 4 個(gè)元素的話(huà),當(dāng)創(chuàng)建循環(huán)結(jié)束時(shí),只有第 4 個(gè)目錄存在,前 3 個(gè)會(huì)被移除;而另一份標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn)首次擴(kuò)容分配 1,K 為 2,那么在 dir_list 內(nèi)有 2 個(gè)元素時(shí),第 1 個(gè)目錄就會(huì)被移除,有 4 個(gè)元素時(shí),只有第 3、4 個(gè)目錄存在,前 2 個(gè)會(huì)被移除。
可見(jiàn)不同的實(shí)現(xiàn)細(xì)節(jié)對(duì)上述代碼運(yùn)行結(jié)果的顯著影響,說(shuō)明 TemporaryDirectory 的實(shí)現(xiàn)存在巨大的缺陷。
當(dāng)然我們可以編寫(xiě)這樣的代碼來(lái)針對(duì)性地解決這個(gè)問(wèn)題:
代碼
//根據(jù)列表創(chuàng)建臨時(shí)目錄
std::vector<TemporaryDirectory> vectd;
vectd.reserve(dir_list.size());//預(yù)先分配內(nèi)存,避免后續(xù)擴(kuò)容
for(const auto &dir : dir_list)
vectd.emplace_back(dir);
這種操作確實(shí)會(huì)讓程序按照預(yù)想工作,但這就像鴕鳥(niǎo)把頭埋進(jìn)沙里,沒(méi)有解決根本的問(wèn)題,況且每次都要記得預(yù)先分配內(nèi)存,顯然是極其麻煩容易出錯(cuò)的。
讓我們來(lái)分析一下這個(gè)問(wèn)題,前面提到一個(gè)關(guān)鍵點(diǎn)———— TemporaryDirectory 沒(méi)有合成的移動(dòng)構(gòu)造函數(shù),為什么編譯器不給我們合成默認(rèn)的移動(dòng)構(gòu)造函數(shù)?這里就牽出 C++ 的一個(gè)重要規(guī)則:三/五/零規(guī)則(The rule of three/five/zero)。這里簡(jiǎn)單概括一下這個(gè)規(guī)則:
三 如果一個(gè)類(lèi)需要自定義析構(gòu)函數(shù),拷貝構(gòu)造函數(shù),拷貝賦值函數(shù)三個(gè)中的任意一個(gè),那么幾乎可以肯定,這個(gè)類(lèi)有必要自定義所有這三個(gè)函數(shù)。
五 自定義的析構(gòu)函數(shù)、拷貝構(gòu)造函數(shù)或者拷貝賦值函數(shù)會(huì)阻止編譯器合成默認(rèn)的移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值函數(shù),如果一個(gè)類(lèi)有移動(dòng)語(yǔ)義需求,那它需要定義所有這五個(gè)函數(shù)。
零 如果一個(gè)類(lèi)沒(méi)有管理資源或所有權(quán)的需求,就不應(yīng)該定義這五個(gè)函數(shù)中的任意一個(gè)。Scott Meyers 在他的一篇博客A Concern about the Rule of Zero中指出,如果不定義這些函數(shù)的意圖是依賴(lài)編譯器合成的默認(rèn)版本,那么就應(yīng)該明確用 =default 標(biāo)明。
現(xiàn)在來(lái)審視一下 TemporaryDirectory 的設(shè)計(jì)需求:1.構(gòu)造時(shí)創(chuàng)建臨時(shí)目錄;2.析構(gòu)時(shí)移除臨時(shí)目錄;3.無(wú)拷貝需求;4.有移動(dòng)構(gòu)造需求。我們現(xiàn)有的實(shí)現(xiàn)只考慮了 1 和 2,并且由于自定義了析構(gòu)函數(shù),默認(rèn)的移動(dòng)構(gòu)造和移動(dòng)賦值函數(shù)無(wú)法合成,現(xiàn)在我們找到了癥結(jié),便可以修改實(shí)現(xiàn):
代碼
class TemporaryDirectory
{
public:
explicit TemporaryDirectory(const std::string &dir): _dir(dir)
{
create_directory(_dir);
}
//注意,標(biāo)注為=default或=delete都被編譯器視為自定義版本
TemporaryDirectory(const TemporaryDirectory &) = delete;
TemporaryDirectory &operator=(const TemporaryDirectory &) = delete;
TemporaryDirectory(TemporaryDirectory &&) = default;
TemporaryDirectory &operator=(TemporaryDirectory &&) = default;
~TemporaryDirectory()
{
//目錄存在則將其刪除
if(directory_exist(_dir) == true)
remove_directory(_dir);
}
bool valid() const
{
return directory_exist(_dir);
}
private:
std::string _dir;
};
void process_data(const std::vector<std::string> &dir_list)
{
//根據(jù)列表創(chuàng)建臨時(shí)目錄
std::vector<TemporaryDirectory> vectd;
for(const auto &dir : dir_list)
vectd.push_back(TemporaryDirectory(dir));
//檢查臨時(shí)目錄是否都已創(chuàng)建
for(const auto &td : vectd)
{
if(td.valid() == false)
return;
}
//使用臨時(shí)目錄存儲(chǔ)臨時(shí)文件
...
}
代碼中提到,標(biāo)注為 =default 或 =delete 都被編譯器視為自定義版本。這引出另一個(gè)問(wèn)題,我們知道用作多態(tài)的基類(lèi)必須聲明一個(gè) virtual 的析構(gòu)函數(shù),即使這個(gè)析構(gòu)函數(shù)什么都不做,使用 =default 標(biāo)明,而這會(huì)阻止編譯器為它合成默認(rèn)的移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值函數(shù),導(dǎo)致它不可移動(dòng),那么所有直接或間接繼承自它的子類(lèi)將都無(wú)法被移動(dòng),我們是不是必須為所有這樣的基類(lèi)添加移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值函數(shù)?按照三/五/零規(guī)則確實(shí)是這樣的,但是經(jīng)過(guò)我在 MSVC(v143 C++14) 和 GCC(v9.3.0 C++14) 上的實(shí)驗(yàn)發(fā)現(xiàn),只要類(lèi)為空或只包含基礎(chǔ)類(lèi)型和指針類(lèi)型,即使定義了析構(gòu)函數(shù)(不必須 =default),都不會(huì)阻止編譯器為它合成默認(rèn)移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值函數(shù)。這令我非常迷惑,但是并未搜索到解釋這個(gè)問(wèn)題的官方文檔,并且我發(fā)現(xiàn)在 StackOverflow 上也有人對(duì)這個(gè)問(wèn)題提問(wèn):1、2,但是并沒(méi)有看到令人完全信服的回答,這個(gè)問(wèn)題也許值得深究。
無(wú)論如何代碼終于能正常工作了,可喜可賀。僅僅是構(gòu)建這么一個(gè)功能簡(jiǎn)易的類(lèi),就花費(fèi)了相當(dāng)多的時(shí)間和精力,這么一想,STL 中那么多通用、穩(wěn)定、順手的模板我們都能隨意取用,實(shí)在是非常慶幸了。
大型項(xiàng)目
在大型 C++ 項(xiàng)目中,資源管理永遠(yuǎn)是繞不開(kāi)的話(huà)題,而且標(biāo)準(zhǔn)庫(kù)提供的設(shè)施可能無(wú)法滿(mǎn)足特定的需求,RAII 思想的應(yīng)用有助于構(gòu)建穩(wěn)定高效的自定義資源管理系統(tǒng)。作者資歷淺薄,沒(méi)什么拿得出手的例子,這里就簡(jiǎn)單介紹兩個(gè)著名的開(kāi)源項(xiàng)目:
OpenSceneGraph 簡(jiǎn)稱(chēng) OSG,是一個(gè)高性能、跨平臺(tái)的 OpenGL 渲染引擎,它通過(guò)場(chǎng)景樹(shù)、狀態(tài)樹(shù)和遍歷器的配合實(shí)現(xiàn)對(duì)場(chǎng)景數(shù)據(jù)和渲染狀態(tài)的自動(dòng)高效管理。它有一套量身定制的資源管理系統(tǒng):OSG 數(shù)據(jù)節(jié)點(diǎn)類(lèi)、狀態(tài)類(lèi)、遍歷器類(lèi)幾乎都間接繼承自一個(gè)名為 Referenced 的基類(lèi),這個(gè)類(lèi)管理著自身的引用計(jì)數(shù),它將析構(gòu)函數(shù) ~Referenced() 定義為 protected 使外部?jī)H能在堆內(nèi)存中創(chuàng)建實(shí)例;另一個(gè)名為 ref_ptr 的模板類(lèi)是一個(gè)典型的 RAII 類(lèi),它以類(lèi)似于 std::shared_ptr 的運(yùn)作方式管理 Referenced 子類(lèi)實(shí)例的生命周期;還有一個(gè)名為 DeleteHandler 的類(lèi)管理資源的釋放行為,可以通過(guò)繼承它來(lái)自定義資源釋放邏輯(比如在性能需求高的時(shí)間段只將待釋放的數(shù)據(jù)收集,在后續(xù)合適的時(shí)機(jī)統(tǒng)一釋放)。這套系統(tǒng)能夠處理渲染時(shí)場(chǎng)景數(shù)據(jù)和狀態(tài)之間錯(cuò)綜復(fù)雜的引用和嵌套關(guān)系,以及自動(dòng)釋放過(guò)期數(shù)據(jù)資源。開(kāi)發(fā)者使用這個(gè)庫(kù)只需要遵循這套系統(tǒng)的規(guī)則,即 Referenced 的子類(lèi)在創(chuàng)建時(shí)交由 ref_ptr 管理,后續(xù)的操作都經(jīng)由這個(gè)管理類(lèi),就可以簡(jiǎn)單地實(shí)現(xiàn)場(chǎng)景管理,避免內(nèi)存泄漏。
OpenSourceComputerVisionLibrary 大名鼎鼎的 OpenCV,它的核心類(lèi) Mat 家族(cv::Mat,cv::UMat,cv::cuda::GpuMat 下文統(tǒng)稱(chēng)為 Mat) 對(duì)內(nèi)存塊的管理同樣是以 RAII 機(jī)制實(shí)現(xiàn)。它們都直接或間接地管理一個(gè)對(duì)內(nèi)存塊的引用計(jì)數(shù),Mat 的拷貝(通過(guò) operator=),興趣區(qū)域(ROI)的引用都只會(huì)增加內(nèi)存塊的引用數(shù)量而不拷貝內(nèi)存;resize、cvtColor 等改變圖像性質(zhì)的操作,會(huì)斷開(kāi)操作對(duì)象與原內(nèi)存的引用關(guān)系,重新分配內(nèi)存,但只要原內(nèi)存尚有其他引用,就不會(huì)被釋放;clone、copyTo 等手動(dòng)的深拷貝操作則會(huì)創(chuàng)建新的內(nèi)存數(shù)據(jù),現(xiàn)有數(shù)據(jù)不會(huì)改變。這些豐富的內(nèi)存管理措施是 OpenCV 安全靈活高效的基本保證。
總結(jié)
1. 如果有資源管理需求,優(yōu)先使用標(biāo)準(zhǔn)庫(kù)的 RAII 設(shè)施,這會(huì)讓編碼更輕松,代碼更可靠。2. 將 RAII 思想應(yīng)用于日常編碼,有助于編寫(xiě)穩(wěn)定、簡(jiǎn)潔、可維護(hù)的代碼。
3. 構(gòu)建自己的資源管理類(lèi)非常有挑戰(zhàn)性,也相當(dāng)有趣味,可以幫助理解語(yǔ)言機(jī)制的細(xì)節(jié)。

浙公網(wǎng)安備 33010602011771號(hào)