あまりにも多くのアサートを書くことは可能ですか?
まあ、もちろんです。[ここで不快な例を想像してください。]ただし、以下に詳述するガイドラインを適用すれば、実際にその限界を押し進めるのに苦労することはないはずです。私もアサーションの大ファンであり、これらの原則に従ってアサーションを使用しています。このアドバイスの多くはアサーションに特別なものではなく、一般的な優れたエンジニアリングの実践のみがアサーションに適用されます。
ランタイムとバイナリフットプリントのオーバーヘッドを念頭に置いてください
アサーションは素晴らしいですが、もしそれがあなたのプログラムを容認できないほど遅くするなら、それは非常に迷惑になるか、遅かれ早かれそれらをオフにします。
アサーションのコストを、それが含まれる関数のコストと比較して測定するのが好きです。次の2つの例を検討してください。
// Precondition: queue is not empty
// Invariant: queue is sorted
template <typename T>
const T&
sorted_queue<T>::max() const noexcept
{
assert(!this->data_.empty());
assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
return this->data_.back();
}
関数自体はO(1)操作ですが、アサーションは O(n)オーバーヘッドをます。非常に特別な状況でない限り、そのようなチェックをアクティブにしたいとは思わない。
同様のアサーションを持つ別の関数を次に示します。
// Requirement: op : T -> T is monotonic [ie x <= y implies op(x) <= op(y)]
// Invariant: queue is sorted
// Postcondition: each item x in the queue is replaced by op(x)
template <typename T>
template <typename FuncT>
void
sorted_queue<T>::apply_monotonic_function(FuncT&& op)
{
assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
std::transform(std::cbegin(this->data_), std::cend(this->data_),
std::begin(this->data_), std::forward<FuncT>(op));
assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
}
関数自体はO(n)操作であるため、アサーションに追加のO(n)オーバーヘッドを追加してもそれほど害はありません。関数を小さな(この場合、おそらく3未満)の一定の係数で遅くすることは、デバッグビルドでは通常は余裕がありますが、リリースビルドではできない場合があります。
次に、この例を検討してください。
// Precondition: queue is not empty
// Invariant: queue is sorted
// Postcondition: last element is removed from queue
template <typename T>
void
sorted_queue<T>::pop_back() noexcept
{
assert(!this->data_.empty());
return this->data_.pop_back();
}
多くの人々は、おそらくこれではるかに快適になりますがO 2と比べて(1)アサーションO(nは前の例)アサーションですが、私の意見では道徳的に同等です。それぞれが、関数自体の複雑さの順序にオーバーヘッドを追加します。
最後に、含まれる関数の複雑さによって支配される「本当に安い」アサーションがあります。
// Requirement: cmp : T x T -> bool is a strict weak ordering
// Precondition: queue is not empty
// Postcondition: if x is returned, then there is no y in the queue
// such that cmp(x, y)
template <typename T>
template <typename CmpT>
const T&
sorted_queue<T>::max(CmpT&& cmp) const
{
assert(!this->data_.empty());
const auto pos = std::max_element(std::cbegin(this->data_),
std::cend(this->data_),
std::forward<CmpT>(cmp));
assert(pos != std::cend(this->data_));
return *pos;
}
ここでは、O(n)関数に2つのO(1)アサーションがあります。リリースビルドでもこのオーバーヘッドを維持することはおそらく問題になりません。
ただし、漸近的な複雑さが常に適切な推定値を与えるとは限らないことに注意してください。実際には、「Big- -O」無視しているためです。
そこで、さまざまなシナリオを特定しましたが、それらについて何ができるでしょうか?(おそらくあまりにも)簡単なアプローチは、「含まれている機能を支配するアサーションを使用しない」などのルールに従うことです。一部のプロジェクトでは機能するかもしれませんが、他のプロジェクトではより差別化されたアプローチが必要になる場合があります。これは、ケースごとに異なるアサーションマクロを使用することで実行できます。
#define MY_ASSERT_IMPL(COST, CONDITION) \
( \
( ((COST) <= (MY_ASSERT_COST_LIMIT)) && !(CONDITION) ) \
? ::my::assertion_failed(__FILE__, __LINE__, __FUNCTION__, # CONDITION) \
: (void) 0 \
)
#define MY_ASSERT_LOW(CONDITION) \
MY_ASSERT_IMPL(MY_ASSERT_COST_LOW, CONDITION)
#define MY_ASSERT_MEDIUM(CONDITION) \
MY_ASSERT_IMPL(MY_ASSERT_COST_MEDIUM, CONDITION)
#define MY_ASSERT_HIGH(CONDITION) \
MY_ASSERT_IMPL(MY_ASSERT_COST_HIGH, CONDITION)
#define MY_ASSERT_COST_NONE 0
#define MY_ASSERT_COST_LOW 1
#define MY_ASSERT_COST_MEDIUM 2
#define MY_ASSERT_COST_HIGH 3
#define MY_ASSERT_COST_ALL 10
#ifndef MY_ASSERT_COST_LIMIT
# define MY_ASSERT_COST_LIMIT MY_ASSERT_COST_MEDIUM
#endif
namespace my
{
[[noreturn]] extern void
assertion_failed(const char * filename, int line, const char * function,
const char * message) noexcept;
}
これで、3つのマクロを使用することができMY_ASSERT_LOW
、MY_ASSERT_MEDIUM
そしてMY_ASSERT_HIGH
代わりに、標準ライブラリの「フリーサイズ」assert
によって支配さもなく、それぞれ自分含む関数の複雑さを支配し、支配どちらも、によって支配されているアサーションのマクロを。ソフトウェアをビルドするときに、プリプロセッサシンボルMY_ASSERT_COST_LIMIT
を事前定義して、実行可能ファイルに含めるアサーションの種類を選択できます。定数MY_ASSERT_COST_NONE
およびMY_ASSERT_COST_ALL
は、どのアサートマクロにも対応せずMY_ASSERT_COST_LIMIT
、すべてのアサーションをオフまたはオンにするための値として使用することを意図しています。
ここでは、優れたコンパイラは以下のコードを生成しないという仮定に依存しています。
if (false_constant_expression && run_time_expression) { /* ... */ }
そして変換
if (true_constant_expression && run_time_expression) { /* ... */ }
に
if (run_time_expression) { /* ... */ }
これは最近の安全な仮定だと思います。
上記のコードを微調整しようとしている場合は、__attribute__ ((cold))
on my::assertion_failed
または__builtin_expect(…, false)
on などのコンパイラ固有の注釈を検討し!(CONDITION)
て、渡されたアサーションのオーバーヘッドを削減してください。リリースビルドでは、関数呼び出しを、診断メッセージを失うという不便さでフットプリントを減らすmy::assertion_failed
ようなものに置き換えることも検討でき__builtin_trap
ます。
これらの種類の最適化は、すべてのメッセージ文字列を組み込むことによって蓄積されるバイナリの追加サイズを考慮せず、それ自体が非常にコンパクトな関数での非常に安価なアサーション(引数として既に与えられている2つの整数の比較など)にのみ関連しています。
このコードを比較する
int
positive_difference_1st(const int a, const int b) noexcept
{
if (!(a > b))
my::assertion_failed(__FILE__, __LINE__, __FUNCTION__, "!(a > b)");
return a - b;
}
次のアセンブリにコンパイルされます
_ZN4test23positive_difference_1stEii:
.LFB0:
.cfi_startproc
cmpl %esi, %edi
jle .L5
movl %edi, %eax
subl %esi, %eax
ret
.L5:
subq $8, %rsp
.cfi_def_cfa_offset 16
movl $.LC0, %ecx
movl $_ZZN4test23positive_difference_1stEiiE12__FUNCTION__, %edx
movl $50, %esi
movl $.LC1, %edi
call _ZN2my16assertion_failedEPKciS1_S1_
.cfi_endproc
.LFE0:
一方、次のコード
int
positive_difference_2nd(const int a, const int b) noexcept
{
if (__builtin_expect(!(a > b), false))
__builtin_trap();
return a - b;
}
このアセンブリを提供します
_ZN4test23positive_difference_2ndEii:
.LFB1:
.cfi_startproc
cmpl %esi, %edi
jle .L8
movl %edi, %eax
subl %esi, %eax
ret
.p2align 4,,7
.p2align 3
.L8:
ud2
.cfi_endproc
.LFE1:
私ははるかに快適に感じています。(例は、GCC 5.3.0を用いて試験し-std=c++14
、-O3
及び-march=native
4.3.3-2-ARCH x86_64のGNU / Linux上のフラグは上記のスニペットに示されていないの宣言であるtest::positive_difference_1st
とtest::positive_difference_2nd
どのIを添加__attribute__ ((hot))
します。my::assertion_failed
と宣言されました__attribute__ ((cold))
。)
それらに依存する関数の前提条件をアサートする
指定したコントラクトで次の関数があるとします。
/**
* @brief
* Counts the frequency of a letter in a string.
*
* The frequency count is case-insensitive.
*
* If `text` does not point to a NUL terminated character array or `letter`
* is not in the character range `[A-Za-z]`, the behavior is undefined.
*
* @param text
* text to count the letters in
*
* @param letter
* letter to count
*
* @returns
* occurences of `letter` in `text`
*
*/
std::size_t
count_letters(const char * text, int letter) noexcept;
書く代わりに
assert(text != nullptr);
assert((letter >= 'A' && letter <= 'Z') || (letter >= 'a' && letter <= 'z'));
const auto frequency = count_letters(text, letter);
各呼び出しサイトで、そのロジックを一度定義します count_letters
std::size_t
count_letters(const char *const text, const int letter) noexcept
{
assert(text != nullptr);
assert((letter >= 'A' && letter <= 'Z') || (letter >= 'a' && letter <= 'z'));
auto frequency = std::size_t {};
// TODO: Figure this out...
return frequency;
}
さらに苦労せずに呼び出します。
const auto frequency = count_letters(text, letter);
これには次の利点があります。
- アサーションコードを記述する必要があるのは1回だけです。関数の目的は、多くの場合複数回呼び出されることであるため、
assert
コード内のステートメントの総数を減らす必要があります。
- 前提条件をチェックするロジックを、それらに依存するロジックの近くに保ちます。これが最も重要な側面だと思います。クライアントがインターフェースを誤用している場合、アサーションを正しく適用することも想定できないため、関数が通知する方が適切です。
明らかな欠点は、呼び出しサイトのソースの場所を診断メッセージに含めないことです。これは小さな問題だと思います。優れたデバッガーを使用すると、契約違反の原因を簡単に追跡できるはずです。
同じ考えが、多重定義された演算子のような「特別な」関数にも当てはまります。イテレータを作成するとき、通常、イテレータの性質上許可されている場合は、イテレータにメンバー関数を与えます
bool
good() const noexcept;
これにより、イテレータを逆参照しても安全かどうかを確認できます。(もちろん、実際には、イテレータを逆参照することが安全でないことを保証することはほとんど常に可能です。しかし、そのような関数で多くのバグをまだキャッチできると信じています。)assert(iter.good())
文でイテレータを使用する場合は、イテレータの実装のassert(this->good())
最初の行として単一を配置operator*
します。
標準ライブラリを使用している場合、ソースコードの前提条件を手動でアサートする代わりに、デバッグビルドでそれらのチェックをオンにします。イテレータが参照するコンテナがまだ存在するかどうかをテストするなど、さらに高度なチェックを実行できます。(libstdc ++およびlibc ++のドキュメントを参照してください(作業中)ください。)
一般的な条件を除外する
線形代数パッケージを書いているとします。多くの関数には複雑な前提条件があり、それらに違反すると、すぐには認識できないような誤った結果がしばしば発生します。これらの関数が前提条件をアサートした場合、それは非常に良いことです。構造に関する特定のプロパティを示す一連の述語を定義すると、それらのアサーションがはるかに読みやすくなります。
template <typename MatrixT>
auto
cholesky_decompose(MatrixT&& m)
{
assert(is_square(m) && is_symmetric(m));
// TODO: Somehow decompose that thing...
}
また、より有用なエラーメッセージが表示されます。
cholesky.hxx:357: cholesky_decompose: assertion failed: is_symmetric(m)
と言うよりもはるかに役立ちます
detail/basic_ops.hxx:1289: fast_compare: assertion failed: m(i, j) == m(j, i)
実際にテストされたものを把握するために、最初にコンテキスト内のソースコードを確認する必要があります。
あなたが持っている場合 class
非自明な不変量と、それはおそらくあなたが内部の状態を台無しにし、あなたがリターン時に有効な状態でオブジェクトを残していることを確実にしたいしている時間までの時間からそれらのアサートに良いアイデアです。
この目的のために、私がprivate
慣習的に呼び出すメンバー関数を定義することは有用であることがわかりましたclass_invaraiants_hold_
。あなたが再実装していたとしましょうstd::vector
(私たちは皆、それが十分ではないことを知っているので)。このような機能を持っているかもしれません。
template <typename T>
bool
vector<T>::class_invariants_hold_() const noexcept
{
if (this->size_ > this->capacity_)
return false;
if ((this->size_ > 0) && (this->data_ == nullptr))
return false;
if ((this->capacity_ == 0) != (this->data_ == nullptr))
return false;
return true;
}
これについていくつか注意してください。
- 述語関数自体は
const
andでありnoexcept
、ガイドラインによれば、アサーションには副作用はありません。理にかなっている場合は、それも宣言しますconstexpr
ます。
- 述語自体は何も主張しません。などの内部アサーションと呼ばれることを意図しています
assert(this->class_invariants_hold_())
。このように、アサーションがコンパイルアウトされている場合、実行時のオーバーヘッドが発生しないことを確認できます。
- 関数内の制御フローは、大きな式ではなく、
if
早いreturn
s を持つ複数のステートメントに分割されます。これにより、デバッガーで関数をステップ実行し、アサーションが発生した場合に不変式のどの部分が破損したかを簡単に確認できます。
愚かなことを主張しないでください
いくつかのことは主張する意味がありません。
auto numbers = std::vector<int> {};
numbers.push_back(14);
numbers.push_back(92);
assert(numbers.size() == 2); // silly
assert(!numbers.empty()); // silly and redundant
これらのアサーションは、コードを少しでも読みやすくしたり、推論しやすくしたりしません。すべてのC ++プログラマーは、std::vector
、上記のコードが見ただけで正しいことを確実にするために、機能するあります。コンテナのサイズを決して主張してはいけないと言っているのではありません。自明でない制御フローを使用して要素を追加または削除した場合、そのようなアサーションが役立ちます。ただし、上記の非アサーションコードで記述されたものを単に繰り返す場合、得られる価値はありません。
また、ライブラリ関数が正しく機能することを主張しないでください。
auto w = widget {};
w.enable_quantum_mode();
assert(w.quantum_mode_enabled()); // probably silly
ライブラリがそれほど信頼できない場合は、代わりに別のライブラリを使用することを検討してください。
一方、ライブラリのドキュメントが100%明確ではなく、ソースコードを読んでその契約についての信頼を得た場合、その「推論された契約」を主張することは理にかなっています。ライブラリの将来のバージョンで破損した場合、すぐに気付くでしょう。
auto w = widget {};
// After reading the source code, I have concluded that quantum mode is
// always off by default but this isn't documented anywhere.
assert(!w.quantum_mode_enabled());
これは、仮定が正しいかどうかを通知しない次のソリューションよりも優れています。
auto w = widget {};
if (w.quantum_mode_enabled())
{
// I don't think that quantum mode is ever enabled by default but
// I'm not sure.
w.disable_quantum_mode();
}
アサーションを悪用してプログラムロジックを実装しないでください
アサーションは、アプリケーションをすぐに殺すに値するバグを発見するためにのみ使用してください。それらの条件に対する適切な対応がすぐに終了することであっても、それらを使用して他の条件を検証しないでください。
したがって、これを書いて…
if (!server_reachable())
{
log_message("server not reachable");
shutdown();
}
…代わりに。
assert(server_reachable());
また、アサーションを使用して、信頼されていない入力を検証したり、それstd::malloc
が実行されなかったreturn
ことを確認したりしないでくださいnullptr
。リリースビルドであっても、アサーションをオフにしないことがわかっている場合でも、アサーションは、プログラムにバグがなく、目に見える副作用がないことを考えると、常に真であるものをチェックすることを読者に伝えます。これが通信したい種類のメッセージではない場合throw
、例外を実行するなどの代替エラー処理メカニズムを使用します。非アサーションチェック用のマクロラッパーがあると便利な場合は、作成してください。「アサート」、「仮定」、「必須」、「保証」などと呼ばないでください。その内部ロジックはassert
、もちろんコンパイルされないことを除いて、forと同じです。
詳しくは
私はジョンLakos'話見つけ正しく行わ守備プログラミング CppCon'14で与えられ、(1 回目の部分、2 回目の部分は)非常に啓発します。彼は、このアサーションで私が行ったよりもさらに、どのアサーションを有効にし、失敗した例外にどのように対応するかをカスタマイズするという考えを取り入れています。