前提条件の確認


8

私は、クライアントが設計上の契約の最後に留まっていることを保証する目的で、入力を検証するためのランタイムチェックを行うかどうかという質問に対する確かな答えを見つけたいと思っていました。たとえば、単純なクラスコンストラクターを考えます。

class Foo
{
public:
  Foo( BarHandle bar )
  {
    FooHandle handle = GetFooHandle( bar );
    if( handle == NULL ) {
      throw std::exception( "invalid FooHandle" );
    }
  }
};

この場合、ユーザーはFoo有効ななしでを作成しようとするべきではないと主張しますBarHandle。がコンストラクタのbar内部で有効であることを確認するのは適切ではないようですFoo。そのFooコンストラクタに有効な が必要ことを単純に文書化した場合BarHandle、それで十分ではありませんか?これは、契約による設計の前提条件を強制する適切な方法ですか?

これまでのところ、私が読んだことはすべて、これについてさまざまな意見があります。50%の人がそれbarが有効であることを確認すると言うようですが、他の50%は私がそれをすべきではないと言っています。たとえば、ユーザーBarHandleが正しいことを確認したが、2番目の(そして不要な)チェックを行う場合を考えてください。もFooコンストラクタの内部で行われています。


回答:


10

これに対する答えは1つではないと思います。必要な主なものは一貫性だと思います- 関数にすべての前提条件を適用する、それらのいずれも適用しないようにします。残念ながら、それはかなりまれです。通常、発生するのは、前提条件について考えて強制するのではなく、プログラマーがコードを追加して、テスト中に違反が発生したために違反が発生した前提条件を適用しますが、失敗の原因となる可能性がある他の可能性を開いたままにすることがよくありますが、テストで発生することはありませんでした。

多くの場合、それは二つの層を提供するために非常に合理的です:任意の前提条件を強制するの試みません「内部」使用のための1、その後、「外」使用のための第二それだけで強制前提条件を、最初に起動します。

ただし、ドキュメント化するだけでなく、ソースノードに前提条件を適用する方が良いと思います。例外またはアサートは、ドキュメントよりも無視するのがはるかに難しく、コードの残りの部分と同期を保つ可能性がはるかに高くなります。


原則として、私はあなたの最後の段落に同意します。これは、同期を維持する必要がある3つの点があることを意味します。ドキュメント、アサート自体、およびアサートが機能していることを証明するテストケース(そのようなことを信じている場合)!
Oliver Charlesworth 2012

@OliCharlesworth:はい、それは同期を保つために第三のものを作成し、しかし、それはそこの意見の相違、一般的に信頼されています一つとして1(ソースコード内の執行)を確立します。それ以外の場合は、通常はわかりません。
Jerry Coffin

2
@JerryCoffin fooがNULL かどうかを確認fooできましたが、無効である可能性があるのはNULLだけではありません。たとえば、-1をにキャストするとFooHandleどうなりますか?ハンドルが無効になる可能性のある方法をすべて確認することはできません。NULLは明白な選択であり、通常チェックされるものですが、決定的なチェックではありません。ここで何を勧めますか?
void.pointer

@RobertDailey:結局のところ、特にキャスティングが関与する場合は特に、あらゆる悪用を保証することはほとんど不可能です。キャスティングを使用すると、ユーザーは本質的に、確認できるすべてのものを破壊できます。私が主に強調しているのは、1)パラメータが適切であると仮定し、テストで問題が発生していることのチェックを追加することと、2)できる限り正確に前提条件を把握し、それらを実行できることの違いです。 。
Jerry Coffin

@JerryCoffinこれは、一般に「良いこと」とは見なされない防御的プログラミングとどう違うのですか?ほとんどの場合、このような防御的なプログラミング手法や、私が見た他の多くの手法は、あまり実用的ではありません。これは、実際の機能とメソッドの実装に焦点を当てるのではなく、同僚の悪いコーディング習慣やその他のものと戦うために作られた設計です。簡単に手に負えなくなるのは、クラス関数全体にボイラープレートロジックを追加する癖だと私は思います。ユニットテストでは、これらの前提条件チェックの必要がなくなると思います。
void.pointer

4

いくつかの異なる概念があるため、これは非常に難しい質問です。

  • 正しさ
  • ドキュメンテーション
  • パフォーマンス

ただし、この場合、これは主にタイプフォールトのアーティファクトです。コンパイラは実際にnullをチェックするため、nullityは型の制約によってより適切に適用されます。それでも、特にC ++では、型システムですべてをキャプチャできるわけではないので、質問自体はまだ価値があります。


個人的には、正確さと文書化が最重要だと思います。速くて間違っていることは役に立たない。速くて間違っているだけの場合は少し良い場合もありますが、あまり効果がありません。

プログラムの一部の部分ではパフォーマンスが重要になる場合がありますが、一部のチェックは非常に広範囲にわたる場合があります(つまり、有向グラフのすべてのノードにアクセス可能とアクセス可能の両方があることを証明します)。したがって、私は二重のアプローチに投票します。

原則1:Fail Fast。これは、防御プログラミング全般の指針であり、可能な限り早い段階でエラーを検出することを提唱しています。方程式にFail Hardを追加します。

if (not bar) { abort(); }

残念ながら、本番環境でハード障害が発生することは必ずしも最善の解決策ではありません。この場合、特定の例外が急いでそこから抜け出すのに役立ち、高レベルのハンドラーが失敗したケースをキャッチして適切に処理するようにします(多くの場合、ロギングして新しいケースを偽造します)。

ただし、これは高価なテストの問題には対応していません。ホットスポットでは、これらのテストのコストが高すぎる可能性があります。この場合、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

0

私がこれについて聞いた引用は:

「あなたがすることを保守的に、あなたが受け入れることを自由にしてください。」

つまり、関数を呼び出すときに引数の規約に従い、関数を記述するときに動作する前にすべての入力をチェックすることになります。

最終的にはドメインに依存します。OS APIを実行している場合は、すべての入力をチェックすることをお勧めします。処理を開始する前に、すべての受信データが有効であると信頼しないでください。他の人が使用するためのライブラリーを作成している場合は、先に進んで、ユーザーにねじ込ませてください(何らかの未知の理由でOpenGLが最初に思い浮かびます)。

編集:オブジェクト指向の意味では、2つのアプローチがあるようです。1つは、オブジェクトがアクセス可能である間、オブジェクトは不正な形式であってはならない(その不変式はすべてtrueでなければならない)ことと、コンストラクターがあることを示すもう1つのアプローチです。すべての不変条件を設定するわけではないので、さらにいくつかの値を設定し、initを終了する2番目の初期化関数を使用します。

マジックの知識を必要としないか、コンストラクターが初期化のどの部分を行わないかを知るために現在のドキュメントに依存しないので、私は前者のほうが好きです。


そのような初期化では、部分的な初期化データを保持し、後で完全に初期化された「usefull」オブジェクトを作成するビルダーオブジェクトを準備します。
user470365 2012
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.