更多元的函式回傳型別:optional 與 outcome


一般當在設計函式介面的時候,都會直接把結果回傳出來;比如說:

int compute(const int input)
{
  return 100 / input;
}

這樣的設計非常直覺,但是像以上面的例子來說,如果 input 是 0 的時候,這個函式就會產生例外狀況(exception)了。

而如果我們想要用 exception 以外的形式,來處理這類的錯誤的話,通常就需要修改函式的介面,讓他可以回傳執行的狀況。


回傳是否正確執行

如果想要用簡單的 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::optionalC++ 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 來處理錯誤的回報,在使用上還算滿方便的~

介面相對乾淨,而且使用上也更為簡單。沒意外的話,以後應該會試著繼續使用吧。
而有玩出什麼想法,就到時候再來寫了。

對「更多元的函式回傳型別:optional 與 outcome」的想法

    • 1. exception 的效能影響不小
      2. 有的時候,錯誤並不是「例外狀況」

      如同 outcome 文件寫的:
      – EC represents a recoverable failure from a lower-layer function
      – EP represents an unrecoverable failure where a C++ exception would ordinarily have been thrown

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Google photo

您的留言將使用 Google 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.