状態チェック関連の強化
C++ 23では、値の有無を表すstd::optionalに加えて、エラーの有無も表す新しい型std::expectedを使えるようになりました。また、std::optionalにモナド的操作のための新しい関数が追加されました。
ここでいう状態チェックとは、値の有無や、エラー発生の有無を判定することです。C言語などでは、有効時(正常時)は値、無効時(エラー時)は0かNULLといったように状態チェックが行われてきましたが、以下の型を使うことで、安全に状態チェックが行えるようになりました。
- std::optional<T>:値が存在するかを表す。値があれば値Tを取得でき、ないという状態も判定できる
- std::expected<T, E>:値が正常かを表す。正常であれば値Tを取得でき、エラーであればその情報Eを取得できる
このようにoptionalでは値の有無を扱いますが、expectedではエラーの有無とその情報まで扱うことができるので、エラー時の理由まで知りたいときにはexpectedを、値の有無さえ分かればよい場合にはoptionalを、と使い分けられます。
std::optionalによる状態チェック[C++ 17]
std::optional型は、値の有無を表すための型です。処理結果そのものを型としてしまうことで、安全な値の取り扱いを可能にします。std::optional型を使わない場合、関数はある値を無効値と定めて、それを返すことで成否を表し、呼び出した側はそれをチェックする必要がありました。
int divide(int a, int b) {
return (b == 0)? INT_MIN : a / b;
}
…略…
auto result = divide(10, 0);
cout << ((result != INT_MIN)? to_string(result) : string("Divide by zero")) << endl; // Divide by zero
関数divideは、除数が0であればINT_MINを返し、そうでない場合は商を返します。この場合、無効値をINT_MINとしているわけですが、これは正しい商かもしれません。このように、特定の値を無効値とすると、正常値と区別が付かないということもあり得ますし、そもそも無効値であるかのチェックも行われない可能性があります。
C++ 17から、値の有効/無効を表せるoptional型が利用できるようになりました。std::optional型を使うと、値の取得にも一定の手続きが必要になります。このため、値の判定を意識的に行うことをプログラマに強制することになります。
#include <optional>
…略…
optional<int> divide_opt(int a, int b) {
return (b == 0)? nullopt : make_optional(a / b);
}
…略…
auto result_opt = divide_opt(10, 0);
cout << (result_opt? to_string(result_opt.value()) : string("Divide by zero")) << endl; // Divide by zero
関数は、optional型を返すものとして定義します。無効時にはnulloptを返し、有効時にはmake_optional関数でoptional型の値を生成して返します。nulloptは、optional型において有効な値を保持していない状態を表すために使用される定数です。呼び出し側では、bool演算子によってnulloptであるかを判定し、有効であればvalue関数で値を取り出してそれを出力し、無効であればその旨を出力します。
エラー処理を安全に扱える新しい型std::expected[C++ 23]
C++ 23からは、optional型を発展させたexpected型を利用できるようになりました。expected型を使うと、値の有効/無効に加えて、エラー時にその理由などを表す値を指定することができます。
#include <expected>
…略…
expected<int, string> divide_exp(int a, int b) { (1)
if (b == 0) {
return unexpected{"Divide by zero"}; (2)
}
return (a / b);
}
…略…
auto result_exp = divide_exp(10, 0);
cout << (result_exp? to_string(result_exp.value()) : result_exp.error()) << endl; // Divide by zero (3)
こちらも、optional型と使い方はほぼ同じです。異なるのは、(1)のように型パラメータにエラー時の情報を表す型を追加すること、エラー情報を(2)のようにunexpectedクラスで生成すること、エラー情報は(3)のようにerror関数で取得することです。
std::optionalにおけるモナド的操作のためのand_then/transform/or_else[C++ 23]
optional型には、C++ 23でand_then/transform/or_else関数が追加されました。これらは、「モナド的操作」のための関数です。
モナド的操作とは、もともとは関数型プログラミングにおける概念で、簡単に表現すると値を連結操作する仕組みです。それぞれ、以下の機能となっています。引数には関数オブジェクトを指定し、値の処理はそこで定義します。
- and_then:有効値なら別の処理を続ける
- transform:有効値なら値を変換する
- or_else:無効値なら代替処理を行う
やや長いのですが、これらの関数の利用例を以下に示します。
// 引数の文字列をint型のOptionalに変換する
optional<int> stringToInt(const string& str) { (1)
try {
return stoi(str);
} catch (...) {
return nullopt;
}
}
// 引数の整数を2倍してint型のOptionalに変換する
optional<int> doubleValue(int value) { (2)
return value * 2;
}
// 引数の整数を文字列に変換する
string intToString(int value) { (3)
return "Value: " + to_string(value);
}
…略…
optional<int> opt = stringToInt("86");
// and_thenの例:有効値ならdoubleValueを呼び出して2倍する
auto result1 = opt.and_then(doubleValue); (4)
cout << result1.value_or(0) << endl; // 172
// transformの例:有効値なら10を加える
auto result2 = opt.transform([](int x) { return x + 10; }); (5)
cout << result2.value_or(0) << endl; // 96
// or_elseの例:無効値なら9999のOptionalとする
auto emptyOpt = stringToInt("Invalid"); (6)
auto result3 = emptyOpt.or_else([] { return optional(9999); });
cout << result3.value_or(0) << endl; // 9999
// 連結処理の例
auto final = opt (7)
.and_then(doubleValue)
.transform(intToString);
if (final) {
cout << final.value() << endl; // Value: 172
}
(1)~(3)は、and_then/transform関数に適用する関数の定義です。(1)は文字列から整数値への変換処理、(2)は整数値を2倍する処理、(3)は整数値の文字列化処理です。
(4)~(6)は、and_then/transform/or_else関数の利用例です。それぞれ、以下の処理内容となります。
なお、and_then関数とtransform関数は同じく値の加工のための関数に見えますが、and_then関数の引数はoptional型を返す関数、transform関数の引数はT(ここではint)型を返す関数である点が異なります。前者は、関数の戻り値となる新しいoptional型を生成し、後者は値そのものを書き換えて、それぞれ処理を継続するという違いがあります。
- (4)and_then:正常値であればdoubleValue関数を呼び出して値を2倍する
- (5)transform:正常値であればラムダ式で値に10を加える
- (6)or_else:無効値であればラムダ式で9999をOptional型として返す
いずれもOptional型を返すので、値の取得にはvalue関数およびvalue_or関数を呼び出します。value_orは、C++ 17から利用可能な、無効値のときに返す値を指定できる関数です。
最後の(7)は、連結処理の例です。それぞれの関数がOptional型を返すので、値の2倍処理、文字列化を連結して行って結果を出力しています。
なお、これらの関数はexpected型には最初から実装されているので、同様の利用が可能です。
まとめ
今回は、C++ 23における標準ライブラリの機能強化について、列挙型関連、状態チェック関連を中心に紹介しました。
次回は、引き続きC++ 23における標準ライブラリの機能強化について、出力関連、部分シーケンス関連、コレクション関連を中心に紹介します。
