std::optionalをさらに便利にするvalue_or()
C++17 以降で個人的に一番好きな機能はstd::optionalなのですが、std::optionalはよく以下のように三項演算子とセットで使われます。
std::optional<std::string> name;
std::cout << "Name is: " << (name ? name.value() : "<missing>") << std::endl;
nameに文字列が入っている場合は、その文字列を出力する、入っていない場合は、違う文字列を出力します。
こういう時は、以下のように C++17 でstd::optionalと一緒に追加されたvalue_orというメンバー関数を使うといいです。
std::cout << "Name is: " << name.value_or("<missing>") << std::endl;
短いですし、三項演算子より読みやすいと思います。
でもvalue_orは名前の通りname.value()で取得できる値が対象なので、残念ながら以下のようにvalue()以外の値を使いたい場合はvalue_orが使えないです。
std::cout << "Name has length: " << (name ? name->length() : 0) << std::endl;
こういう場合にはC++23 からstd::optionalに追加されるモナド的操作と呼ばれるtransform, and_then, or_elseというメンバー関数使えます。
どれにも引数を指定できるのですが、有効値か無効値によってその関数をコールし、その戻り値をstd::optionalとして返してくれます。
指定できる関数に関して以下の違いがあります。
-
transformにはT function(T)のような関数が指定できる -
and_thenとor_elseにはstd::optional<T> function(T)のような関数が指定できる
指定された関数がコールされる条件も以下の違いがあります。
-
transformとand_thenは、無効値の場合はそのまま無効値を返しますが、有効値の場合は指定された関数をコールし、有効値を渡し、戻り値を返します。 -
or_elseは、有効値の場合はそのまま有効値を返しますが、無効値の場合は指定された関数をコールし、戻り値を返します。
実際に使ってみると、もう少し分かりやすくなると思います。
transformにはstd::string::lengthを以下のように直接指定できます。
std::cout << "Name is: " << name.transform(&std::string::length).value_or(0) << std::endl;
- 有効値の場合は、
std::string::lengthがコールされ、有効値が渡せれ、その戻り値が返ってきます。 - 無効値の場合は、そのまま無効値が返ってきます。
どちらの場合も戻り値がstd::optional<std::string>型になります。nameが無効値の場合はそのまま無効値が返ってくるので、value_orをコールしています。
有効値の場合はそのまま返して、無効値の場合は0を返します。
and_thenには引数に指定できるのは戻り値がstd::optional<T>型の関数である必要があるため、ラムダ式関数を使っています。その中でstd::string::lengthをコールし、その戻り値をstd::optional<std::string>にラッピングして返します。
引数に指定した関数は「有効値の場合に呼び出される」ため、その関数の引数で有効値を受け取ります。
std::cout << "Name is: " << name.and_then([](std::string text) { return std::optional(text.length()); }).value_or(0) << std::endl;
それ以外はtransformと同じ挙動になります。
or_elseにはand_thenと同様にラムダ式関数で指定しますが、実装が完全に逆になります。
and_thenとは逆に無効値の場合に関数が呼び出されるため、引数で受け取るようなことはありません。
std::cout << "Name is: " << name.or_else([] { return std::optional<std::string>(""); })->length() << std::endl;
or_else()の部分は以下のようになります。
- 有効値の場合は、そのまま有効値(つまり
name)が返ってきます。 - 無効値の場合は、指定したラムダ式関数がコールされて、その戻り値を返します。(
std::optional<std::string>(""))
どちらの場合も戻り値は有効値になるため、最後には普通にstd::string::lengthをコールできます。
この例だとtransformは使いやすいですが、and_thenとor_elseは少し無理矢理な感じるかもしれません。またtransformの場合も三項演算子の方が分かりやすいと感じる人もいると思います。
transform, and_then, or_elseはどちらかというと、std::optional型の変数を連続で複数の関数に渡したい時に使うための関数であって、こういうシンプルな例では複雑すぎるかもしれないですね。(その話はまた別の記事にでも書いてみようと思います)
なんだかvalue_or自体よりほかの説明が長くなってしまいましたが、ともかくvalue_orは非常に使いやすいので、是非使ってみてください!
Discussion