如何優雅的處理(或忽略)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&(Someone, isFuckedByInt, 0, (float)1);

不管怎樣,藉助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& Fuck(ErrorCode(*shit)(TArgs ...args, R* result))
{
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&(Someone);
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)

//錯誤處理

......


推薦閱讀:

python程序報錯後除了try except之外有沒有好的辦法再次啟動?

TAG:編程語言 | 編程 | 代碼 | C | 異常處理 |