如何優雅的處理(或忽略)c++函數返回值代表的錯誤?
在不使用exception的時候,函數可以選擇使用返回值作為操作是否成功的標誌,或者返回值的一部分值域作為錯誤碼(比如scanf之類的函數)。那麼,要處理這類錯誤,我一般就會簡單的寫出如下的代碼:
int ret = f();
if (ret &< 0) error(ret);這裡,如果錯誤是我無法控制的部分,那麼error函數的處理往往就是簡單的忽略或者列印error log。但是,當需要連續調用幾個這樣的函數,並且後續函數需要使用前面函數的返回值的時候,代碼就會充滿了if-else,比如:
int ret1 = f();
if (ret1 &< 0) { error(ret1); } else { int ret2 = g(ret1); if (ret2 &< 0) { error(ret2); } else { int ret3 = h(ret2); ... } }在有些語言中,如果能夠簡單的忽略錯誤,那麼可以寫出比較乾淨的代碼,比如Scala或者Haskell可以這樣寫(這裡省略monad的實現)
for {
ret1 &<- f() ret2 &<- g(ret1) ret3 &<- h(ret2) }那麼C++針對這種情況有什麼簡單通用的寫法呢?
這種情況應該中途就返回,寫成
int ret1 = f();
if (ret1 &< 0)
{
error(ret1);
return
}
int ret2 = g(ret1);
if (ret2 &< 0)
{
error(ret2);
return;
}
int ret3 = h(ret2);
很多人避免中途返回,是因為後面需要釋放已經分配的資源。在 C++ 中,應該一旦分配資源,就立即使用 RAII 或者 ScopeGuard 來自動清理。比如
FILE* file = fopen(filePath.c_str(), "rb");
if (file == NULL)
{
return data;
}
ON_SCOPE_EXIT
{
fclose(file);
};
或者
char* buffer = malloc(buf_size);
ON_SCOPE_EXIT
{
free(buffer);
};
這樣的寫法,可以保證資源永遠都會被釋放。就可以放心地中途退出。這種寫法也使得重構代碼更容易,可以將資源分配和釋放的代碼一起移動,因為它們是靠在一起的。
那些使用 goto, 或者 while (true) break 的,可以緩解一部分問題。但在C++中,沒有ScopeGuard 通用和優雅。
我很希望C++可以定義出一個類似 Go 語言或者 Swift 語言的 defer 特性,或者將 ScopeGuard 添加到標準庫中。這樣就不用不同項目各自實現風格各異的 ScopeGuard 了。
關於 ScopeGuard 更詳細的內容,參考這篇文章 ScopeGuard 介紹和實現 - 黃二少碎碎念 - 知乎專欄----------------
我個人是不喜歡異常 exception 的。以往我一直覺得異常是很好的特性,但現在覺得異常並非一定是好的。異常的問題在於發生異常的地方與處理異常的地方分離開來,真正處理的時候難以收集到足夠的信息。異常也容易濫用,很容易只拋出異常而不處理,或者乾脆在最外層接收所有異常,再列印出異常信息。這樣的話,也就失去了異常處理的意義。
用錯誤碼雖然看起來死板麻煩,但會是可控的。
有些人會說,構造函數出現錯誤怎麼辦,那時沒有返回值,也就沒有錯誤碼。這種情況下,只要簡單讓構造函數不會出現錯誤就行了,構造函數會出現錯誤,是因為在裡面做太多事情了。比如我就不會在構造函數中打開文件,而是提供 open 或者 init 的成員函數,在裡面打開。
----------------
再來說說使用錯誤碼的介面設計。這樣設計介面很常見:
typedef enum
{
ErrorCode_OK = 0,
ErrorCode_Err1 = 1,
} ErrorCode;
ErrorCode doSomething(Arg0 arg0, Arg1, arg1);
doSomething 通過錯誤碼來判斷錯誤。但因為有很多函數會返回 bool 或者指針來表示對錯,絕大多數非 0 表示成功。上面那種函數介面設計,會容易將函數誤用成:
if (!doSomething(arg0, arg1))
{
// 錯誤處理,
// 其實這樣應該是調用成功的, 被誤用了
}
包含錯誤碼的介面應該設計成
bool doSomething(Arg0 arg0, Arg1, arg1, ErrorCode* error);
但需要簡單判斷對錯,就這樣使用
if (!doSomething(arg0, arg1, nullptr))
{
// 錯誤處理
}
但需要考慮更詳細的錯誤信息,就這樣調用
ErrorCode errCode;
if (!doSomething(arg0, arg1, errCode))
{
// 使用errCode來進行更詳細的錯誤處理
}
你需要
goto
不要因為一些教條徹底放棄這種有效的方法。
Update-----我以為這種方式都是常識了,不過還是放上來吧, err = foo1();
if (err != 0)
goto err1;
err = foo2();
if (err != 0)
goto err2;
.....
err2:
cleanup2();
err1:
cleanup1();
...
根據輪子哥的回答整理出來的代碼。PS:知乎在評論里怎麼插入帶格式的代碼啊?如果可以的話我也不想做「回答自己的問題」這麼low的事情,全部放進問題描述里又顯得太長了……
下面借用 @vczh 的回答中的Someone函數,而Fuck可以簡單的如下實現template&
R Fuck(ErrorCode shit(T1, T2, R*), bool isFucked(ErrorCode), T1 arg1, T2 arg2) {
R result;
ErrorCode err = shit(arg1, arg2, result);
if (isFucked(err)) throw exception("error code is : " + err);
return result;
}
這樣的好處是調用的時候,模版參數都可以inference出來。比如這樣調用:
auto result = Fuck1(Someone, isFuckedByInt, 0, (float)1);
這個的問題是shit的參數個數是固定的,想要支持1-N個參數,就需要1-N份重載。另外,可以看到內置類型的自動轉換沒有了,必須加一個強制類型轉換。
輪子哥的實現中用了template parameter pack,也就是可以如下實現:template&
R Fuck(ErrorCode shit(Args..., R*), bool isFucked(ErrorCode), Args ...args) {
R result;
ErrorCode err = shit(args..., result);
if (isFucked(err)) throw exception("error code is : " + err);
return result;
}
這樣做的好處是不用N份重載就能處理不同參數個數的shit了。但是引入了一個新的問題,就是Args在Fuck的參數列表裡expand了兩遍。在調用的時候,就算傳的參數在這兩處是一致的,也沒法inference參數類型,必須自己顯式指定。調用的時候就變成了這樣:
auto result = Fuck2&
不管怎樣,藉助exception,可以把我的問題描述中的嵌套if-else去掉,並且不會打斷邏輯的表達。比如:
try{
auto ret1 = Fuck(f, isFuckedByInt, arg1);
auto ret2 = Fuck(g, isFuckedByInt, ret1);
auto ret3 = Fuck(h, isFuckedByInt, ret2);
return ret3;
} catch (exception e) {}
(輪子哥的命名真是……)
然而有的時候被逼無奈只能放棄exception,不管是要保證和C的兼容性,還是其他的什麼原因(參見對使用 C++ 異常處理應具有怎樣的態度? - 編程),那麼 @下愚給出的goto方法也可以使用。int ret1;
int err = f(ret1);
if (err &< 0) goto err1;
int ret2;
err = g(ret1, ret2);
if (err &< 0) goto err2;
int ret3;
err = h(ret2, ret3);
if (err &< 0) goto err3;
...
err3:
err2:
err1:cleanup()
這裡真正返回值需要提前聲明(ret1,ret2,ret3),if...goto語句也混雜在邏輯中間(其中的f,g,h依次調用),錯誤處理也需要寫在同一個函數里。如果用宏來簡化一下,可能不會太丑,但畢竟沒有exception版本來的乾淨。不過效率可能比帶exception版本的高一些,對老版本編譯器兼容性也好一些。
渣渣過來抖抖機靈。
原料:短路求值。bool is_error(int r){....}
int res;(is_error(res = f())) || (is_error(res = g(res))) || (is_error(res = h(res)));安全無副作用有沒有。
還有?:也可以搞出來。不過限制蠻多的。exception在這裡就剛好跟monad一樣啊,分割語句的「operator ;」就是&>&>=。你只要在普通代碼進入monad的時候寫一個catch,你就完成了從一個代數結構到另一個代數結構的優雅的變換了。
沒有其他辦法比這個在形式上更加優雅了。如果你堅持不用exception,那就自己用longjmp和setjmp去模擬吧,然後你會發現無論怎麼寫都會有內存泄露。
當然了,要把所有的代碼都改一遍不現實,所以可以使用一個fucker,哦不,helper函數:
template&
function&
{
return [=](TArgs ...args)
{
R result;
auto error = shit(args, result);
if (error) throw Exception(error);
return result;
};
}
ErrorCode Someone(int a, float b, double* c)
{
if (a == 0) return ERROR_BITCH;
*c = a + b;
return ERROR_OK;
}
auto FuckSomeone = Fuck&
auto result = FuckSomeone(0, 1); //這裡拋異常
你要的是多個相互依賴subroutines如何handle中間異常的情況,我不知道是否實現一個chain的小東西是否困難,(其實就是一個系統,輸入是多個subsystem,輸出是一個執行狀態)但是感覺上還是很容易的。矛盾之處在於,你既要拆分成subroutine,然而這些subroutine又要相互作用,互相依賴,所以這應該不會是一個針對某一個ErrorCode f(...)的解決方案,而是一堆ErrorCode f的集成方案
int ret;
while (true) {
ret = f();
if (ret &< 0) break;
ret = g(ret);
if (ret &< 0) break;
ret = h(ret);
....
return true;
}
error(ret);
return false;
看看go的處理如何。。
返回錯誤本身就不優雅,所以怎樣都無所謂啦。
如果你說的問題就是例子中那種簡單情況,就用薯條的辦法是最好的,很多人都嫌exception代價大.那麼這種辦法雖然形式不是3條語句,但是從邏輯是等價的,恐怕也比Haskell那種方式效率要高.
do{
err = foo1();
if (err != 0) break;err = foo2();if (err != 0) break;}while(0)//錯誤處理......推薦閱讀: