一般當在設計函式介面的時候,都會直接把結果回傳出來;比如說:
int compute(const int input) { return 100 / input; }
這樣的設計非常直覺,但是像以上面的例子來說,如果 input 是 0 的時候,這個函式就會產生例外狀況(exception)了。
而如果我們想要用 exception 以外的形式,來處理這類的錯誤的話,通常就需要修改函式的介面,讓他可以回傳執行的狀況。
<!–more–>
回傳是否正確執行
如果想要用簡單的 bool 來做判斷的話,或許可以寫成:
bool compute(const int input, int& output) { if (input == 0) return false; output = 100 / input; return true; }
也就是把本來的輸出作為函式的引數、來在函式內做修改;而回傳的則是函式執行的狀態。
使用時,則會變成下面的形式:
int iRes; if (compute(input, iRes)) std::cout << "OK: " << iRes << std::endl; else std::cout << "Error" << std::endl;
這樣的修改法,必須要先定義一個變數(iRes)、傳入函式裡面讓他做修改,使用上稍嫌麻煩;而且函式的定義上如果沒弄好,也容易讓人搞不清楚哪個是輸出用的,個人並不喜歡這樣的設計。
使用指標
另一種判斷是否執行成功的方法,是把回傳值改成指標的形式。例如:
int* compute_ptr(const int input) { if (input == 0) return nullptr; return new int(100 / input); }
使用上呢,則會變成下面的形式:
auto pRes = compute_ptr(input); if (pRes) { std::cout << "OK: " << *pRes << std::endl; delete pRes; } else std::cout << "Error" << std::endl;
這樣的設計比較大的問題,應該就是指標本身的危險性了。
如果這個回傳值需要留著很久的話,就很有可能會難以判斷什麼時候需要釋放了。
(當然,使用 smart pointer(參考)或許可以解決問題)
std::optional
而如果想要在使用上更直覺的話,可以考慮使用 C++17 的 std::optional 來做處理。
std::optional(C++ Reference)基本上有點類似指標,他提供了一個允許「沒有值」的容器。
而使用 std::optional 的函式,在使用時須要引用 <optional> 這個 header 檔;函式則可以寫成下面的形式:
std::optional<int> compute_opt(const int input) { if (input == 0) return std::nullopt; return 100 / input; }
使用的時候,則是:
auto optRes = compute_opt(input); if(optRes) std::cout << "OK: " << *optRes << std::endl; else std::cout << "Error" << std::endl;
在個人來看,和使用指標相比算是更為直覺一點吧~
此外,std::optional 也提供了 value_or() 這種可以提供預設值的函式,在某些情況下也可以簡化寫法。
比如說,本來要用判斷式來處理的寫法大概是:
int x = 0; if (optRes) x = *optRes;
透過 value_or() 可以直接簡化成:
int y = optRes.value_or(0);
在使用上算是方便不少,同時相較於讓函式直接回傳預設值,使用上也更有彈性。
而由於 Boost 也有提供 optional 的功能,所以實際上許多裡面的函式庫也有用到這類的用法~像是之前介紹的 Boost.Convert 就有用上了。
std::error_code
而如果希望回傳的狀態可以更詳細,也可以透過 std::error_code 來處理;這樣可能可以寫成下面的樣子:
int compute(const int input, std::error_code& ec) { if (input == 0) { ec = std::make_error_code(std::errc::invalid_argument); return 0; } ec.clear(); return 100 / input; }
使用上,則會變成:
std::error_code ec; int iRes2 = compute_ec(input, ec); if(ec) std::cout << "Error: " << ec.message() << std::endl; else std::cout << "OK: " << iRes2 << std::endl;
老實說,還滿繁瑣的。
而且如果要針對不同的錯誤來處理的話,也不能透過 std::optional 來做處理。
Boost.outcome
這時候,如果想要稍微簡化上面的寫法的話,Boost.Outcome(官網)就有用了!
Boost.Outcome 主要目的,是透過一個特別型別,讓函式可以同時回傳值、或是錯誤的狀態;在個人來看,在某種程度上或許可以視為 std::optional 的強化版本。
提供了 outcome<> 和 outcome::result<> 兩種型別可以使用。
基本上,outcome::result<> 算是比較簡單的版本,可以視為把本來的回傳值和錯誤代碼(error code)整合在一起的容器。
而 outcome<> 則是 outcome::esult<> 的擴展版本,除了可以回復(recoverable的錯誤代碼外,還有不可回復(unrecoverable)的例外(exception)。
要使 outcome 的話,要先引入 <boost/outcome.hpp> 這個 header 檔。
由於 outcome 的 API 有改版過,所以目前建議使用的是 boost::outcome_v2 這個 namespace 下的東西。
官方是建議使用 namespace 的別名來處理:
namespace outcome = BOOST_OUTCOME_V2_NAMESPACE;
result<>
Outcome 的 result<> 基本上是三個參數的 template 列別:
result<T, EC = varies, NoValuePolicy = policy::default_policy<T, E, void>>
其中:
- T 是回傳值得型別
- EC 則是函式錯誤的理由的型別,預設是 boost::system::error_code
- NoValuePolicy 則是沒有回傳時的處理方法
而如果要使用 std::error_code 來做為錯誤的回報型別的話,則可以使用預設的 std_result<T>。
上面的程式,則可以改成:
#include <boost/outcome.hpp> namespace outcome = BOOST_OUTCOME_V2_NAMESPACE; outcome::std_result<int> compute_res(const int input) { if (input == 0) return std::make_error_code(std::errc::invalid_argument); return 100 / input; }
使用的時候,則可以寫成:
auto orRes = compute_res(input); if (orRes) std::cout << "OK: " << orRes.value() << std::endl; else std::cout << "Error: " << orRes.error().message() << std::endl;
這邊 orRes 的型別,就是 std_result<int>,他可以直接透過 if() 來判斷是否有值。
而要取得他的值的話,則是要透過 value() 這個函式。
當錯誤的時候,則可以透過 error() 來取得錯誤的理由,在這邊得到的結果就是函式內回傳的 std::error_code。
在個人來看,這樣的使用是比直接使用 std::error_code 來的方便、直覺的!
outcome<>
outcome 的部分,大致上和 result<> 一樣,不過他的 template 參數又多了一個、變成四個:
outcome<T, EC = varies, EP = varies,
NoValuePolicy = policy::default_policy<T, EC, EP>>
可以看到,這邊多的一個是 EP,他的預設值會是 boost::exception_ptr。
而如果想要全部都使用 std 的版本(std::error_code + std::exception_ptr)的話,則可以直接使用 std_outcome<T>。
感覺上,這個用法似乎是可以用來整合函式內部的錯誤回報機制?不過個人目前還沒有用到,所以就還沒認真研究了。(官方文件)
以目前玩了一下來看,感覺透過 outcome::std_result<> 搭配 std::error_code 來處理錯誤的回報,在使用上還算滿方便的~
介面相對乾淨,而且使用上也更為簡單。沒意外的話,以後應該會試著繼續使用吧。
而有玩出什麼想法,就到時候再來寫了。