前提:假設所有的析構函數、移動構造和移動賦值、swap函數都具有noexcept保證。

問題:在實踐中如何將順序串聯幾個具有強異常安全保證的函數組合而成的新函數快速重構成強異常安全的?

例如

void SomeClass::SomeFunc()
{
m_Data1 = f1();
m_Data2 = f2();
m_Data3 = f3();
// f1、f2、f3和對應的operator=都是強異常安全的
}

如何重構實現f3失敗時回退f1和f2的操作?


你需要的是事務……

異常安全並不保證整個函數的原子性,後者要求高於前者。

這個改動不是幾行代碼的事,是業務相關,架構相關的。


來,我教你怎麼操作。

void SomeClass::SomeFunc()
{
auto tmp1 = f1();
auto tmp2 = f2();
auto tmp3 = f3();

// critical line, anything below must not throw
using std::swap;
swap(m_Data1, tmp1);
swap(m_Data2, tmp2);
swap(m_Data3, tmp3);
}

總體上Strong exception safety大致就是這種思路,你上面先在臨時變數上操作,一通猛於虎之後過一條虛擬的「線」,這條線之下的東西一概不能亂扔果皮紙屑。這條線下面的操作通常就是各種swap或者各種move,所以你給出的前提是必要的,有這些了事情就很好辦。而線上面的步驟失敗了,你的臨時變數自然就被unwound掉,對成員變數等不會造成影響。

這種操作的代價是會有性能開銷,特別當swap/move並不很廉價的時候(比如你要操作的是std::array這種),或者你的f函數需要對成員變數同時進行讀寫(inplace edit變成copy再swap)。所以並不是每個函數都應該strong exception guarantee,很多應用場景下basic guarantee也是可以的。但是沒有任何異常安全保證(也就是說你調用的代碼哪天給你穿一個異常出來你就開膛破肚漏內存)是絕對不行的,哪怕你的公司是那種禁用異常迅雷不及掩耳盜鈴的公司,你也應該全面使用RAII。

PS:舉個不需要強異常安全的例子,比如說你處理一個web request,你要操作的對象生命週期與request相同,處理到一半炸異常了你就對外拋一個500之類的出去然後結束請求處理。這時候這個對象炸壞掉就壞掉好了,反正炸了就要析構了。當然,前提是basic exception safety,也就是炸壞了不泄露任何資源。

PS2:也有用這套套路解決不了的情況,比如你的f裡面有對某個global/static對象的寫操作等副作用,或者更嚴重的,給外面某個服務發了請求做了什麼寫入操作,這種沒有什麼特別通用的好招,前者還可以考慮用個scope guard加反函數來試圖消掉,後者估計就上分散式事務吧。這是業務和系統設計層面的事,不是exception safety能解決得了的了。

PS3:這裡也引出了一個問題,就是LZ說的要有nothrow move或者swap。我見過一些被某米國軍方公司的邪教style guide洗腦洗到喪心病狂的同學寫的代碼(比如我廠的不少遺產代碼),明明是一些個帶有data bundle性質的類,非要每個類上來先無腦寫宏禁止copy禁止move,然後因為不能copy不能move還要傳遞出去就裸new、裸指針傳得滿天飛。看得我內心是崩潰的。你如果有一個field既不能copy又不能move,只能mutate,那自求多福吧,除了讓該類的作者寫個反函數以外基本沒有什麼好辦法提供強異常安全。所以說早日現代化吧,不要抱著C++98年代的邪教guide當聖旨了,都8021了唉。

——更新——

有同學說解決方案是不使用異常。好!我就在等這個。現在我來簡單show一下為什麼在這個問題裏不使用異常完全是掩耳盜鈴,解決不了任何問題。

首先,因為要不使用異常,還要有能夠report error、可能會失敗的fx三兄弟,於是直接return是不行了auto也用不了了(除非用C++17 std::optional或者boost::optional,如果你這麼先進我相信你不會對異常是這個態度,就不多說了)。這裡為了簡單我們假設三個member的類型叫做T。fx三兄弟要寫成:

int f1(T* out);
int f2(T* out);
int f3(T* out);

然後我們來重寫題主的someFunc,我們要保持和原版本的語義等價,也就是說如果fx三兄弟有彙報error的,someFunc應該將錯誤繼續上報上去供上層代碼處理。

int SomeClass::SomeFunc()
{
int ret = 0;
ret = f1(m_Data1);
if (ret != 0) {
return ret;
}
ret = f2(m_Data2);
if (ret != 0) {
return ret;
}
// ...

已經不用再寫下去了。發現什麼問題沒?如果f2的返回值不是0,發生什麼事情了?是不是f1已經把m_Data1修改了,f2一通操作猛於虎,猛一半跪了,整個對象進入了半截狀態回不去了?

這和題主原始例子裏f2拋出異常有區別麼?

沒有。

解決方案?簡單,還是上面那套,開臨時變數,操作,然後swap,做到「Strong errorcode guarantee」。其實等價於Strong exception guarantee,只是錯誤彙報方式不一樣、代碼一個簡潔一個醜而已。

於是問題來了,你都做到強異常安全了,你還怕異常幹什麼呀?


贊同 sin1080 。補充一點,對於fx有副作用的時的操作。

當fx有副作用時,它的結果不光體現在局部變數上,那麼我們需要有一個反操作,勉強叫reversefx。它和fx同參,做的事情與fx相反。比如

int f1(int n) {
g_some += n; //用全局變數模擬外部操作。
}
int reversef1(int n) {
g_some -= n; //同上
}

這與在銀行賬戶操作中沖正操作類似。

然後利用scope_exit退出時作反向操作即可。


Exception-Safe Coding in C++?

exceptionsafecode.com圖標

打開一本資料庫事務設計的書來看看?通用的機制不太可能知乎這幾頁紙說的清楚吧。


應該可以通過改編譯器的配置文件

開啟異常安全代碼通過編譯,

開你用什麼開發環境或什麼編譯器

vs這種用的多的應該有解決方案的


謝邀。

將狀態和資源申請解耦,延遲狀態改變。


不太明白你的意思,怎麼回退,執行過的代碼還能當沒執行過?

手寫啊,除非你有時光機,否則不會有通用的辦法?


推薦閱讀:
查看原文 >>
相關文章