C++ 23 可以回傳值或錯誤的 std::expected

之前在《更多元的函式回傳型別: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 畢竟還沒正式定案,之後會不會有變動?這就不知道了。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *