C++23: <expected> ヘッダー; 予期せぬことを期待しよう - DEV コミュニティ
関数から複数の値を返す必要があるとき、どうしますか?データ構造のインスタンスを返しますか?出力変数を使いますか?それとも例外を投げてエラーコードを取り除きますか?

明確な選択ではありません。

C++23では、<expected> ライブラリを使って、ステータスコードと期待される戻り値を同時に洗練された方法で扱うための標準化されたソリューションを提供します。

昔の方法

名目上の戻り値とステータスコードを扱うことは、いくつかの解決策があった問題ですが、どれも完璧ではありません。

出力パラメーターを使用

ステータスコードと名目上の結果(つまり期待される値)を両方返す最も古くてよく使われる方法は、「出力」変数を介しています。エラーコードを返し、その組みが出力変数を介して帰り値を返す関数を考えてみてください。逆の場合もあるかもしれませんが、よくあるのはこの方法です。

以下が例です:


int fetchCitiesFromDb(const std::string& query, std::vector<std::string>& results) {
    // ...
};

// ...

std::vector<std::string> v;
int errorCode = fetchCitiesFromDb(aQuery, v);

フルスクリーンモードを終了する

このようなAPIは理解しにくいです。デフォルトでは、出力は戻り値にあり、パラメータを入力として解釈することを期待しますが、このソリューションではそうではありません。単純ではありません。

例外を使用

別のオプションは自明です!例外を使います。まあ、可能なら。すべての環境では許されておらず、一部のチームはその使用を禁止しています。

コストがかかると主張する人もいます。いつものように、依存しています。データベースから読み取ったり、ネットワーク経由でデータを送ったりする時間のほとんどを費やしているなら、それほど重要ではありません。

一方で、常に真実であることは、それが完全に通常の制御フローを回避し、例外を使用してコードについて推論することが非常に困難であるということです。多くの場合、例外を誤用し、何度もエラーをログに記録し、例外を再投げます。

std::variantstd::optional の使用

この燃え盛る問題に対する現代の回答は、C++17によって導入されたデータ構造のいずれかを使用することです。std::optionalを選択した場合、値の存在がすべて期待通りにうまくいったことを意味します。値がない場合、つまり戻り値が std::nullopt の場合は、エラーケースを処理する必要があります。

このアプローチには少なくとも1つの欠点があります。異なるエラーケースを区別する方法がありません。

std::expected が提供するもの

std::expected はまさにそれを提供します!期待される戻り値またはエラーコードのいずれかを格納する自由と、異なる値にアクセスするための非常に綺麗な optional 風のAPIを得られます。

もう少し詳しく見てみましょう。std::expectedstd::expected<T, E> として2つのパラメータを取るテンプレートクラスです。最初のもの、Tは名目上の戻りタイプ(期待されるタイプ)を表し、Eはエラーの場合に返すタイプです。つまり、エラーコードに限らず、エラーを表現するのに役立つと思う任意のタイプが可能です。

std::optional のように、次のAPIを提供します:

  • has_value()
  • operator bool()
  • operator*()
  • operator->()
  • value()
  • value_or()

さらに、別のAPI関数も提供します:

  • error()

それぞれが何をするか見てみましょう。

std::expected オブジェクトがタイプ T の値を含む場合、has_value()operator bool() の両方が true を返します。さもなければ false を返します。

どう使うか?もうサポートされているのか?

<expected> ヘッダーはすでに gccmsvc によってサポートされています。コンパイラのサポートレベルは C++ Reference で常に確認できます。

では、std::expected を実際に使ってみましょう!

次に、std::expected を返す型として使用する fetchCitiesFromDb を作業してみましょう。

enum class ErrorCode { InvalidConnectionString, InvalidQuery };

std::expected<std::vector<std::string>, ErrorCode> fetchCitiesFromDb() {
    // ... ?
}

代替手段にどうフィットするか?例外をいつ使うか?

これまで見てきた後、代替策とどのように相互作用するか、どのようなエラーハンドリング方法を選択すべきかを自問する時です。

提案された「非例外」のエラーハンドリング方法よりも、std::expected が明らかに優れていると私は思います。

結論

両方の正常な戻り値とステータスコードを返すことが想定されている関数を何十年も書くことは苦痛でした。独自のデータ構造を作るか、戻り値に加えてパラメータも使用する必要がありました。それからC++17が登場し、std::optionalstd::variant の両方を導入していくつかの代替案を提供しましたが、それらはエラーハンドリングを扱うために設計されていませんでした。

C++23はエラーコードを含む std::optional 風のAPIを持つ標準化されたデータ構造である std::exceptional をもたらします。読みやすく、使いやすい解決策です。APIの可読性を向上させるでしょう!

より深くつながる

この記事が気に入ったら、いいねボタンを押し、私のニュースレターに登録しTwitterでつながりましょう!

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/sandordargo/c23-the-header-expect-the-unexpected-5fgk