之前在《更多元的函式回傳型別:optional 與 outcome》這篇文章中,曾經提過在函式需要回傳計算的結果,但是又可能需要回傳處理的狀態(包含錯誤)的時候,除了可以使用簡單的 C++17 的 std::optional 來處理沒有辦法回傳值的狀況外,下面就是一個簡單的例子:
#include <iostream> #include <optional> std::optional<int> compute_opt(const int input) { if (input == 0 || input > 100) return std::nullopt; return 100 / input; } int main(int argc, char** argv) { auto optRes = compute_opt(101); if (optRes) std::cout << "OK: " << *optRes << std::endl; else std::cout << "Error" << std::endl; }
但是這樣的方法僅能知道是否有錯誤,而無法知道到底為什麼出錯,其實在比較複雜的環境下,也不算夠用。
所以後來也有另外介紹了 Boost 的 Outcome 這個函式庫,透過他來處理較為複雜的錯誤狀態回傳。
而在尚未正式定案的 C++23,則是也引進了 <expected> 這個函式庫(參考),來處理這樣的狀況。他的定位基本上應該算是 std::optional 的擴展功能,讓他除了除了可以處理沒有值得狀況、更可以在沒有值的時候,附加額外的錯誤狀態。
目前 <expected> 在 C++ Reference 的文件還不完整,所以如果要看比較詳細的內容的話,可能就要看對應的草案 P0323R11(連結)了。
<expected> 這個函式庫主要的型別是:
template<class T, class E> class expected {}
他有兩個 template 引數,其中的 T 代表正常狀況下要回傳的資料,而 E 則是代表錯誤時回傳的錯誤訊息資訊;可以看的出來,他的設計上和 Boost Outcome 的 result<> 比較接近。
在使用上,他就可以變成類似下面的形式:
#include <iostream> #include <expected> enum class EError { DivideByZero, ValueTooLarge }; std::expected<int, EError> compute_expected(const int input) { if (input == 0) return std::unexpected<EError>(EError::DivideByZero); if(input > 100) return std::unexpected<EError>(EError::ValueTooLarge); return 100 / input; } int main(int argc, char** argv) { auto eRes = compute_expected(2); if (eRes) std::cout << "OK: " << *eRes << std::endl; else { switch (eRes.error()) { case EError::DivideByZero: std::cout << "Error: Divide By Zero" << std::endl; break; case EError::ValueTooLarge: std::cout << "Error: Value larger than 100" << std::endl; break; } } }
函式回傳的型別是 std::expected<int, EError>, 代表在正確處理的時候,會回傳一個 int;而當出現錯誤時,則會回傳前面定義出來的列舉型別 EError,在這邊算是示意性地定義了兩種錯誤。
在可以正確處理的時候,基本上可以直接回傳計算完後的值、不用另外處理;但是當要回傳錯誤的時候,則是要明確地回傳 std::unexpected<> 才行,這點算是和 Boost Outcome 一個比較不一樣的地方。
當我們拿到結果後,可以簡單地透過 if(eRes) 來判斷他是否有正確處理;如果有的話,就可以直接透過 &eRes、或是 eRes.value() 來取得回傳值。
而當錯誤的時候,則可以透過 eRes.error() 來取得錯誤的狀態,做進一步的處理、或是把錯誤告訴使用者。
由於這邊的錯誤型別是 template 的,所以也可以對應傳統用 int 來做為錯誤代碼的狀況;而如果有必要,其實也可以透過 C++11 的 std::error_code 來時做一個符合標準架構的錯誤管理代碼機制。
此外,這邊也有提供 value_or() 這樣的函式,來快速地處理在錯誤發生時,要給一個預設值的狀況。
像下面的例子,在傳入 101 的時候是會觸發錯誤的,但是這邊可以透過 value_or(),在出錯的時候都統一給予「1」這個值,來做後續的處理。
auto eRes = compute_expected(101); std::cout << eRes.value_or(1) << std::endl;
這樣的寫法,在許多時候、尤其是在寫 parser 的時候,會是相當方便的~
理論上目前 g++ 12.1 應該是有支援了,而 Visual Studio 則是要到目前還在預覽階段的 2022 17.3 才會支援。
而 Boost Outcome 也有提供官方範例、來透過 Boost Outcome 實作出 std::expected<>(官方範例、不過缺少 value_or()),所以如果想要先用上這個未來的標準函式庫的話,其實應該也是可以試試看了;這樣一來,到時候編譯器正是支援 C++23 的時候,應該也可以很快速地切到標準函式庫的版本。
只是因為 C++23 畢竟還沒正式定案,之後會不會有變動?這就不知道了。