アサーションとは|目的と活用例

アサーションとは

アサーション(assertion)はプログラムの処理や条件の成立を表明するための手法や機能のことです。アサーションはプログラムの診断を実現する機能でもあり、これによってプログラムの意図しない動作を検出することが可能となります。

assert(1 == 1); // 正常
assert(1 == 2); // 表明違反(プログラムが停止する)

アサーション機能の主な役割は、与えられた式の結果が偽(false)となった際に、エラーや例外を発生させたり、プログラムの実行を中断させたりするというものです。

アサーションチェック

アサーションを活用することで、プログラムの処理が正しい条件によって動作しているということを保証することができます。プログラムが決められた条件を満たさない挙動を取った場合には、アサーションの機能によって警告文の表示や実行時エラーが引き起こされます。それによってプログラムのバグや問題の早期発見や対処に繋げることが可能となります。

このようにしてプログラムの正当性を検証する手法のことを「アサーションチェック(assertion check)」と呼びます。多くのプログラミング言語ではassertマクロやassert関数、assert文を利用することで、アサーションチェックを実現することができます。またこれらの機能やツールは「アサーションチェッカ」と呼ばれています。

// assertマクロ(C言語, C++)
assert(1 == 2); // Assertion failed: (1 == 2), function main, file /main.c, line 3.

// assert文(Java)
assert 1 == 2;  // Exception in thread "main" java.lang.AssertionError at java.main(Assert.java:3)

// assertメソッド(JavaScript)
console.assert(1 == 2, "一致しません"); // Assertion failed: 一致しません

アサーションの活用例

nullの受け取りを拒否する

assertを活用することで、nullを受け付けない関数を表現することができます。

void f(char *a, char *b) {
   assert(a); // 第一引数のみ空ポインタの受け取りを制限する
}
f("a", "b");  // ok
f("a", NULL); // ok
f(NULL, "b"); // fail(実行時エラーが発生)

これによって、開発の段階でnullに対する安全性をある程度確保することが可能となります。

もっとも、nullの許容や拒否の明示が行えるような言語では、そちらを使うのが一般的です。

// オプショナル機能(Swift, Kotlin)
func f(a: String, b: String?) {}

// Nullability属性(C言語拡張)
void f(char *_Nonnull a, char *_Nullable b) {}

起こり得ない処理の表明

基本的には起こりえないような処理や分岐を、意図的に表明しておくことも有効です。

int get_os_code(const char *type) {
   if (type == "Windows") return 100;
   if (type == "Mac")     return 300;
   
   assert(!"起こり得ない"); // Linux対応は未定
   return -1;
}

万が一の場合の迅速な対応や仕様の見直しにも繋がります。また開発中やデバッグ中に「対応を後回しにしたい処理をassertで明示しておく」という一時的なコーディング・テクニックとしての活用も考えられます。

なお、このような起こりえない処理の表明を行う際には、NOTREACHEDマクロを作成することもオススメです。

#define NOTREACHED() assert(!"NOTREACHED")

開発の規模に応じて、エラーメッセージのみを表示させるようにしたり、ログをとるように拡張するのも良いでしょう。

#define NOTREACHED(message) fprintf(stderr, "NOTREACHED: %s\n", message)

ちなみに、未到達コードに対する警告を抑止させるために/* NOTREACHED */コメントが用いられることもあります。

バグや問題の根本原因を表明する

特定の状態の変数や引数を取り得ない処理を記述する際には、事前にアサーションチェックを行うことが有効です。

void f(char *s) {
   assert(s != NULL); // アサーションチェック
   for (int i = 0; i < length(s); i++);
}

// int length(char *s) { return *s ? length(s + 1) + 1 : 0; } // 文字列の長さを返す関数。詳しくはstrlen関数を参考

assertによる事前チェックを行わない場合、length関数側にNULLが渡されることになりますが、その際、実行時エラーはlength関数側で発生することになります。この場合、バグの原因究明は大変困難なものとなります。lengthがどの関数から呼ばれた際に発生したエラーなのかが分からなくなる場合があるためです。

もっとも、デバッガのスタックトレースを解析することで、問題の原因を究明することも可能であるため、上記のような単純な処理であれば、わざわざアサーションチェックを行う必要はありませんが、複雑な処理を記述する際には、assertによる事前チェックが極めて重要となることがあります。

バグの原因となりうる状態や処理は事前にアサーションチェックしておくことが重要です。アサーションは想定内のバグを明示する目的としても活用できるのです。

アサーションチェックで問題の根本原因を表面化させる事で、根本原因の副作用によって生じうる直接原因そのものを回避することにも繋がります。これによって直接原因に対する余計な調査や無駄な対応も回避されます。より詳しい話は次項で行います。

バグの発生を事前に回避する

アサーションの適切な利用は、原因の特定が難しいバグや不定な処理の発生を回避する効果があります。これによって想定不要なバグに対する余計な原因究明を回避することにも繋がります。

例えば、以下のitoc_fast関数は09のint型の数値を'0''9'のchar型の数字に変換する関数ですが、オーバヘッドを回避するために、あえて範囲チェックやデフォルト処理を行わない特殊な設計が用いられています(このような速度重視の設計は、組み込みシステムやハードリアルタイムシステムで用いられることがあります)

char itoc_fast(int i) {
   int s[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
   return s[i];
}

C言語の場合、配列オブジェクトに範囲外の添字が与えられた場合には、多くの場合エラーや例外が発生しません。代わりに不定な値が返されます。

printf("%c", itoc_fast(1));   // '1'
printf("%c", itoc_fast(9));   // '9'
printf("%c", itoc_fast(123)); // 'x' (不定な値)

このような不定な値が、他の関数の引数や戻り値として使われてしまう恐れもあります。これによって不定な値が様々な処理に伝染/伝播することとなります。

char buggyChar = itoc_fast(123);
int crazyInt = buggyChar - '0';
return crazyInt * 10 + buggyChar;

伝播した先で、不定な値によってバグが引き起こされた場合、そのバグの根本原因を特定することは大変困難となります。バグの原因となった不定な値が、「いつ」「どのタイミングで」発生して渡ってきたものなのか、その追求が難しくなるケースがあるためです。

このような副作用による問題を回避するためには、不定な値が発生しうる関数の側で、事前にassertによる診断を行っておくことが有効です。

char itoc_fast(int i) {
   assert(0 <= i && i <= 9); // 事前にチェック
   int s[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
   return s[i];
}

これで、不定な値を受け取った際の対応が可能となります。assertの警告によってバグの早期発見が可能となり、迅速な対応にも繋がるのです。

なお、本番環境でassertを無効化すれば、本来の目的であったゼロオーバヘッドの利点を享受することも可能となります。開発段階でのアサーションによる水際対策を徹底することで、本番時の安全性をある程度保証することも可能となります。

ただし、冒頭のitoc_fast関数のような未定義動作を伴う設計は、安全性や汎用性の観点ではあまり好ましくないものであり、一般的な開発では用いるべきではありません。本来であれば、きちんと境界チェックを行い、デフォルト値を返すようにし、使う側でそれらの結果を判定させることが求められます。

char itoc(int i) {
   if (0 <= i && i <= 9) { // 境界チェック
      int s[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
      return s[i];
   }
   return '\0'; // デフォルト値
}

char c = itoc(-1);
if (c != '\0') {
   /* 正常処理 */
} else {
   /* 異常処理 */
}

例外処理機能が活用できるプログラミング言語では、それらの機能を活用するのも良いでしょう。例外処理機能は本来、この手の問題に対処するために存在しています。

char itoc(int i) {
   if (i < 0 && 9 < i) throw Exception(); // 例外送出
   int s[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
   return s[i];
}

try {
   char c = itoc(-1);
   /* 正常処理 */
} catch (Exception e) { // 例外捕捉
   /* 異常処理 */
}

例外処理機能はアサーションの代替として利用できるだけでなく、より役割が明確で、かつ安全性の高い、厳格な仕組みとして機能します。

広告