例外なしでコンストラクターのエラーを処理する方法?

C++ サブレディットを閲覧しているときに、次のコメントに遭遇しました。

子コメントで現在行われている例外の議論に飛びつくつもりはありません.C++コンストラクターがエラー処理のために例外を必要とするのは悲しいことだと彼が言った部分に焦点を当てるだけです.アプリケーションに例外サポートがあり、エラーを報告する必要があるコンストラクターがあります。どうしますか?

例外の使用について強い意見がある場合の免責事項:私は例外の使用を支持しません。

問題

エラー処理の最も顕著な方法は、戻り値を使用することです。しかし、コンストラクターには戻り値がないため、実行できません。これが、C++ に例外が追加された理由の 1 つです。

ただし、関数から値を返す方法は複数あります。出力パラメータを使用できます:

foo(arg_t argumwent, std::error_code& ec)
{
 if (initialization_failed(argument))
 ec = …;
}

追加の引数である出力パラメーターを受け入れます。初期化が失敗した場合、例外をスローする代わりに、単純にエラー コードを設定します。その後、呼び出し元はエラー コードをチェックしてエラーを処理できます。

ただし、この手法には複数の欠点があります。最も明らかな欠点は、誰もエラー コードのチェックを強制されないことと、簡単に忘れてしまうことです。しかし、もっと微妙な欠点があります。

コンストラクターで例外がスローされた場合、オブジェクトは完全には構築されませんでした。これは、そのデストラクタが呼び出されないことを意味します。さらに、エラー状態のオブジェクトにアクセスする方法はありません。例外は、ローカル変数をすぐにアンワインドします。

優れた保証があります:コンストラクター呼び出しが返された場合、オブジェクトは有効であると見なされます.空の保証:すべてのクラス オブジェクトは有効なリソースを所有する必要があります。移動セマンティクスの問題を解決/回避したと仮定すると、コンストラクターを簡単に実装できます:

foo(arg_t argument)
: resource(acquire_resource(argument))
{
 if (!resource)
 throw no_resource();
}

保証により、これにより、各オブジェクトにリソースが確実に含まれるようになります。例外がスローされると、オブジェクトはありません。

エラーコードに出力パラメーターを使用すると、これらすべてが失われます。デストラクタが呼び出されます。これは、考えられるすべてのエラー状態を処理する必要があることを意味します。しかし、エラー状態のオブジェクトを使用しないように注意する必要があります。空にならないことを保証することは不可能です。すべてのオブジェクトには、有効と無効の少なくとも 2 つの状態があります。

問題の回避策

例外とエラー コードは、回復可能なエラー処理メカニズムです。エラーを呼び出し元に報告し、プログラムを続行できるようにします。ただし、回復可能なエラー処理メカニズムには、エラーを報告する方法が必要です。オブジェクトの保証を犠牲にすることなくコンストラクターを作成します。

したがって、コンストラクターでエラーを処理する最も簡単な方法は、回復可能なエラー処理メカニズムを使用しないことです。メッセージを 07 に出力するなど、回復不可能なものを使用してください。 14 を呼び出す .

この投稿で概説したように、このメカニズムはとにかくプログラマー エラーのようなものにより適しています。 33 の場合は例外 は負です。デバッグ アサーションを使用してください。

さらに、メモリ不足など、本質的に回復できないエラーがあります。その場合は、ハンドラー関数を呼び出してプログラムを中止するだけです。ユーザーは、メッセージをユーザーに表示する方法をカスタマイズできますが、それを処理するために多くのことを行うことはできません。

ただし、これらは回避策にすぎません。一部のエラーは回復可能であり、処理できません。問題を解決しましょう。

ソリューション

コンストラクターで例外なしで回復可能なエラー処理メカニズムを使用できない場合は、コンストラクターを使用しないでください。

待って、聞いて。

46 を提案しているわけではありません 関数またはそのようなもの。これを行うと、RAII のすべての保証が失われ、おそらく 56 も必要になります。 これは、デストラクタが無効なオブジェクトに対して呼び出されるためです。これで、C API を同様に記述できるようになりました。

RAII は難しくなく、生活をとても楽にし、不利な点はありません。まあ、コンストラクター例外のことを除けば、それはそうです.

C++ の機能の 1 つは、すべての言語機能を自分で実装できることです。コンパイラがそれを実行します。それでは、コンストラクタを見てみましょう。

基本的には、次の 2 つの手順があります。まず、オブジェクトに生メモリを割り当てます。次に、そのメモリ内でコンストラクタを呼び出して、オブジェクトを作成します。2 番目の手順で例外がスローされた場合は、スタックの巻き戻しに入ります。そうでない場合は、デストラクタの呼び出しをスケジュールします。

これは 60 でのアプローチ方法でもあります と 74 メソッドは機能します。オブジェクト コンストラクターは何もしないので、コンパイラーはメモリの割り当てだけを行います。89 そして 90 そこで実際にオブジェクトを作成します。

しかし、2 つの状態をオブジェクト自体の一部にしたくはありません。構築されたすべてのオブジェクトは有効である必要があり、無効な状態の複雑さは別の場所に移動する必要があります。無効な状態を導入できるラッパーが必要です。オブジェクトがそこにありません。

このようなラッパーは 109 と呼ばれます 、たとえば。コンストラクターを使用する代わりに、コンストラクターを提供しないため、オブジェクトを作成できなくなります。オブジェクトを作成する唯一の方法は、112 を使用することです。 たとえば関数.しかし、これは通常の関数なので、戻り値を使用できます.特に、そ​​れは 122 を返します オブジェクト:

optional<foo> make(arg_t argument, std::error_code& ec)
{
 auto resource = make_resource(argument);
 if (resource)
 return foo(resource);
 return {};
}

すべてが成功した場合は、オブジェクトを返すことができます。ただし、エラーの場合は、無効なオブジェクトを返す必要はありません。代わりに、空のオプションを返すことができます。

この API は次のように使用できます:

std::error_code ec;
auto result = foo::make(arg, ec);
if (result)
{
 // everything alright
 …
}
else
 handle_error(ec);

これで、オブジェクトを取得するたびに、有効であることが保証されます。無効な状態は、処理をより適切に実装できる場所に移動されます。したがって、すべてのメンバー関数とデストラクタは、無効な状態を処理する必要はありません。つまり、 135 まで 関数はオブジェクトを作成するだけです。つまり、コンストラクターを呼び出します。これ以上問題が発生することはありません。

エラー報告の改善

出力パラメーターとしての戻り値は少しぎこちないです。

より良い方法は、それを戻り値に統合することです.代わりに 149 を返します 、「値またはエラーのいずれか」クラスを使用します。提案された std::expected はそれを行い、エラーをよりエレガントに処理できます。

コピー コンストラクターはどうですか?

この手法は「通常の」コンストラクターには適していますが、コピーの場合はどうでしょうか?それでも失敗する可能性がある操作です。

2 つの解決策があります:コピー操作を提供せず、移動のみ - これは (通常) 失敗しません - または同じ手法を再度使用します。150 を提供します 161 同じことを行い、再び 173 を返す関数 /182 など

結論

例外がない場合、保証を犠牲にすることなくコンストラクターからエラーを報告することは不可能です。可能な場合は、代替の回復不可能なエラー報告方法を使用してください。

それが当てはまらない場合は、198 を入力してください オブジェクトを作成する唯一の方法として機能します。オブジェクトを直接返すのではなく、オプションの型を返します。実装を慎重に作成して、実際の 201 コンストラクターは、操作が失敗しない場合にのみ呼び出されます。その後、例外を使用する場合と同様に、すべてのオブジェクトが有効になります。