C++23 std::optional 的新函式

| | 0 Comments| 09:20
Categories:

之前 Heresy 就已經有在《更多元的函式回傳型別:optional 與 outcome》和《C++ 23 可以回傳值或錯誤的 std::expected》這兩篇文章介紹過 C++17 的 std::optional 和 C++23 的 std::expected 了。

不過,Heresy 是在看到《Functional exception-less error handling with C++23’s optional and expected》後,才發現原來 C++23 還有為 std::optional 增加新功能、讓他在使用上更方便!

這邊新增的有三個 monadic operation:and_then()transform()or_else(),這三者都是接受一個可呼叫的物件作為引數,來處理 std::optional 的不同狀況。(參考

or_else()

其中,or_else() 是在沒有值的狀況下、去執行指定的函式、並回傳同樣的型別。

這樣的設計應該算是讓開發者更簡單地依序去測試各種方法,來取得最後的結果。

下面是一個例子:

#include <iostream>
#include <optional>
 
std::optional<int> getValueMethod1()
{
  return std::nullopt;
}
 
std::optional<int> getValueMethod2()
{
  return 2;
}
 
int main()
{
  int iValue = 0;
 
  std::optional<int> optVal = getValueMethod1();
  if (optVal)
  {
    iValue = *optVal;
  }
  else
  {
    optVal = getValueMethod2();
    if (optVal)
      iValue = *optVal;
  }
 
  std::cout << iValue << std::endl;
}

這邊有 getValueMethod1()getValueMethod2() 兩個函式可以用來取得值,而且都有可能失敗;這邊為了測試,是讓 getValueMethod1() 固定回傳 std::nullopt

main() 裡面的程式,則是希望先試試看 getValueMethod1() 是否可以成功,如果失敗的話再試試看 getValueMethod2(),然後最後的結果就是 iValue 這個變數;以上面的程式來說,最後的結果會是 2。

而這邊的程式在 C++23,就可以透過 or_else() 來做簡化,可以一行解決:

int iValue = getValueMethod1().or_else(getValueMethod2).value_or(0);

在個人來看,這樣的設計在符合使用情境的狀況下,確實是滿方便的。


and_then() 和 transform()

相較於前面的 or_else() 是在沒有值的狀況下才會去執行,另外兩個 and_then()transform() 則是只有在有值得狀況下才會去執行所傳入的可呼叫物件;而他們會去處理 std::optional 的值並回傳、同時也可以改變回傳值的型別。

兩者的差別主要應該是對於傳入的可呼叫物件的回傳值的形式有不同的要求。

and_then() 的函式需要回傳 std::optional,算是把整個使用 std::optional 系統串起來的感覺。

而給 transform() 的函式則是則是回傳一般的值,然後 transform() 會再包一層 std::optional;基本上算是讓本來沒有使用 std::optional 的既有函式也可以拿來用。

下面是一個例子:

#include <iostream>
#include <string>
#include <optional>
#include <algorithm>
 
std::string toLower(std::string s)
{
  std::transform(s.begin(), s.end(), s.begin(),
    [](unsigned char c) { return std::tolower(c); });
  return s;
}
 
std::optional<std::string> getTextInRange(const std::string& sText,
  std::string_view t1, std::string_view t2)
{
  size_t uPos1 = sText.find(t1);
  if (uPos1 != std::string::npos)
  {
    uPos1 += t1.size();
    size_t uPos2 = sText.find(t2, uPos1);
    if (uPos2 != std::string::npos)
      return sText.substr(uPos1, uPos2 - uPos1);
  }
  return std::nullopt;
}
 
std::optional<std::string> getQuoted(const std::string& sText)
{
  return getTextInRange(sText, "\"", "\"");
}
 
std::optional<std::string> getParentheses(const std::string& sText)
{
  return getTextInRange(sText, "(", ")");
}
 
std::optional<std::string> getText()
{
  return R"tt(test: "Hello world (Hi)")tt";
}
 
int main()
{
  auto s = getText().and_then(getQuoted).and_then(getParentheses)
            .transform(toLower);
  if (s)
    std::cout << *s << std::endl;
}

這邊就是先透過 getText() 來取得文字,並在有值的時候,依序去執行 getQuoted()getParentheses()toLower() 這幾個函式,最後得到的結果會是「hi」。

透過這樣的機制,就可以把許多函示串聯起來、一次處理掉,而不需要個別去檢查他們的回傳值了。

而這邊要注意的,是由於 toLower() 回傳的型別是 std::string,所以要用 transform()、不能用 and_then()

另一方面,如果把 getQuoted() 這種回傳 std::optional<std::string> 的函式拿來搭配 transform() 的話,得到的結果則會變成 std::optional<std::optional<std::string>>、也就是多加了一層。


此外,這邊的函式也不一定都得輸入字串、回傳字串,實際上也可以把它處理成其他型別,只要全部串的起來就好。

下面就是一個空的例子:

#include <iostream>
#include <optional>
 
struct A {};
struct B {};
struct C {};
 
std::optional<C> convBC(B)
{
  return C();
}
 
std::optional<B> convAB(A)
{
  return B();
}
 
int main()
{
  std::optional<A> optA;
  std::optional<C> optC = optA.and_then(convAB).and_then(convBC);
}

不過不知道為什麼,這邊好像沒辦法處理 function overloading?這邊如果把 convBC()convAB() 都改名成 conv() 的話,雖然一般使用可以靠引數型別的不同來區分,但是搭配 and_then() 就編譯不過了。


另外,在 std::expected 裡,除了同樣有這三個函式外,還多了一個 transform_error()

他們的使用方法基本上大致和 std::optional 的版本相同,不過 or_else() 的部分變成需要能接受錯誤資訊才行,所以在使用上還是不大一樣。

而多出來的 transform_error() 則是用來處理、轉換 std::expected 的錯誤;他會接受本來的錯誤(例如 std::error_code)、然後回傳另外的錯誤、也可以改變代表錯誤的型別。

使用的狀況應該會類似下面的樣子:

#include <iostream>
#include <expected>
#include <system_error>
 
std::expected<double, std::error_code> func()
{
  return std::unexpected<std::error_code>(
    std::make_error_code(std::errc::bad_message));
}
 
std::string err(const std::error_code& ec)
{   return ec.message();
}
 
int main()
{
   std::expected<double, std::error_code> a = func();
   std::expected<double, std::string> b = a.transform_error(err);
   if (!b)
     std::cerr << "Error: " << b.error() << std::endl;
}

這邊就是透過 err() 這個函式,在 a 有錯誤的時候,將本來型別為 std::error_code 的錯誤資訊轉換成型別為 std::string 的錯誤資訊。

不過目前 C++ Reference 裡面也還沒有針對 std::expected 這幾個函式說明(網頁)、Visual C++ 2022 的正式版也還不支援,上面是拿最新版的 g++ 來玩的就是了。

Leave a Reply

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