私は、クライアントが設計上の契約の最後に留まっていることを確認する目的で、入力を検証するための実行時チェックを行うかどうかという質問に対する確かな答えを見つけたいと思っていました。たとえば、単純なクラスコンストラクターを考えます。
class Foo
{
public:
Foo( BarHandle bar )
{
FooHandle handle = GetFooHandle( bar );
if( handle == NULL ) {
throw std::exception( "invalid FooHandle" );
}
}
};
この場合、ユーザーは、有効なFoo
なしでBarHandle
を構築しないでください。 bar
がFoo
のコンストラクター内で有効であることを確認するのは適切ではないようです。 Foo
のコンストラクタにvalidBarHandle
が必要であることを単純に文書化した場合、それで十分ではありませんか?これは、契約による設計の私の前提条件を強制する適切な方法ですか?
これまでのところ、私が読んだことはすべて、これについてさまざまな意見があります。 50%の人がbar
が有効であることを確認すると言っているようですが、残りの50%は私がやるべきではないと言っています。たとえば、ユーザーがBarHandle
を確認する場合を考えてください正しいですが、2番目の(そして不要な)チェックもFoo
のコンストラクター内で行われています。
これに対する単一の答えはないと思います。必要な主なものは一貫性だと思います-関数にall前提条件を強制するか、またはanyを強制しようとしないかのどちらかです。残念ながら、それはかなりまれです。通常、発生するのは、前提条件を考えて強制するのではなく、プログラマーがコードを追加して、テスト中に違反が発生して違反が発生した前提条件を適用しますが、失敗を引き起こす可能性のある他の可能性を開いたままにすることがよくありますが、テストで発生することはありませんでした。
多くの場合、2つの層を提供することはかなり合理的です。1つは前提条件を適用しようとしない「内部」の使用、次に「外部」使用の2番目はjustが前提条件を適用してから呼び出します。最初。
ただし、ドキュメント化するだけでなく、ソースノードに前提条件を適用する方が良いと思います。例外またはアサートはmuchドキュメントよりも無視するのが難しく、コードの他の部分と常に同期している可能性が高くなります。
いくつかの異なる概念があるため、これは非常に難しい質問です。
ただし、この場合、これは主にtype障害のアーティファクトです。コンパイラは実際にnullをチェックするため、nullityは型の制約によってより適切に適用されます。それでも、特にC++では、型システムですべてをキャプチャできるわけではないので、質問自体はまだ価値があります。
個人的には、正確さと文書化が最重要だと思います。速くて間違っていることは役に立たない。速くて間違っているだけでも時々少し良くなりますが、テーブルに多くをもたらすこともありません。
ただし、プログラムの一部ではパフォーマンスが重要になる場合があり、一部のチェックは非常に広範囲にわたる場合があります(つまり、有向グラフのすべてのノードにアクセス可能とアクセス可能の両方があることを証明します)。したがって、私は二重のアプローチに投票します。
原則1:Fail Fast。これは、防御的プログラミング全般の指針であり、可能な限り早い段階でエラーを検出することを提唱しています。 Fail Hardを方程式に追加します。
_if (not bar) { abort(); }
_
残念ながら、本番環境でハード障害が発生することは必ずしも最善の解決策ではありません。この場合、特定の例外は急いでそこから抜け出すのに役立ち、高レベルのハンドラーが失敗したケースをキャッチして適切に処理するようにします(ほとんどの場合、ロギングして新しいケースを偽造します)。
ただし、これはexpensiveテストの問題には対応していません。ホットスポットでは、これらのテストのコストが高すぎる可能性があります。この場合、DEBUGビルドでのみテストを有効にするのが妥当です。
これにより、素晴らしくシンプルなソリューションが得られます。
SOFT_ASSERT(Cond_, Text_)
DEBUG_ASSERT(Cond_, Text_)
2つのマクロがこのように定義されている場合:
_ #ifdef NDEBUG
# define SOFT_ASSERT(Cond_, Text_) \
while (not (Cond_)) { throw Exception(Text_, __FILE__, __LINE__); }
# define DEBUG_ASSERT(Cond_, Text_) while(false) {}
#else // NDEBUG
# define SOFT_ASSERT(Cond_, Text_) \
while (not (Cond_)) { \
std::cerr << __FILE__ << '#' << __LINE__ << ": " << Text_ << std::endl; \
abort(); \
}
# define DEBUG_ASSERT(Cond_, Text_) SOFT_ASSERT(Cond_, Text_)
#endif // NDEBUG
_
私がこれについて聞いた引用は:
「あなたがしていることを保守的にし、あなたが受け入れることを自由にしなさい。」
つまり、関数を呼び出すときに引数の規約に従い、関数を記述するときに動作する前にすべての入力をチェックすることになります。
最終的にはドメインに依存します。 OS APIを実行している場合は、すべての入力をチェックすることをお勧めします。処理を開始する前に、すべての受信データが有効であると信頼しないでください。他の人が使用するためのライブラリーを作成している場合は、先に進んで、ユーザーが自分でねじ込ませるようにします(OpenGLは最初に何らかの未知の理由で頭に浮かびます)。
編集:OOの意味では、2つのアプローチがあるようです。1つは、オブジェクトがアクセス可能である間、オブジェクトが不正な形式であってはならない(その不変式はすべてtrueである)ことです。そして、すべての不変条件を設定しないコンストラクターがあることを示すもう1つのアプローチは、さらにいくつかの値を設定し、initを終了する2番目の初期化関数を使用することです。
マジックの知識を必要としないか、コンストラクターが初期化のどの部分を行わないかを知るために現在のドキュメントに依存しないので、私は前者のほうが好きです。