アサート | Programming Place Plus C言語編 第30章

トップページC言語編

このページの概要

以下は目次です。


標準エラー

ここまでの章では、なにかしらの出力を行うときには、標準出力を対象にしていました。実は標準出力のほかにも、出力先として選択できる対象があります。ここではその1つである標準エラー (standard error) を取り上げます。

標準エラーは、なんらかのエラーや問題が発生したときに、その情報を出力するために使います。

標準出力と標準エラーの具体的な出力先は、ほとんどすべての環境で「画面」です。そのため、どちらを使っても結果は変わらないようですが、プログラムの正常な進行の中で出力するメッセージは標準出力へ出力し、エラーメッセージは標準エラーへ出力するという使い分けをすることが多いです。その理由は2つあります。

1つには、標準エラーへの出力は、ただちに出力されるということです。そういうと標準出力はそうでないようですが、標準出力への出力は、すぐに実行される保証はありません。これにはバッファリング (buffering) という仕組みが関係しています。バッファリングの詳細な解説は第43章まで保留しておきますが、簡単にいえば、出力させようとするデータをいったんどこかに蓄えておき、ある程度溜まってきてから一気に出力しているということです。ほとんどの環境では、標準出力ではバッファリングが有効であり、標準エラーでは無効になっています1。バッファリングされていると、プログラムが異常な動作によって不意に停止してしまったとき、出力されないまま終了する可能性がありますから、重要度が高いエラーメッセージなどは標準エラーに出力したほうが良いということになります。

2つ目の理由は、出力先は変更できるからです。(そのようにソースコードを書けば)プログラマーの意思で変更できますし、プログラムを実行するユーザーの側でもリダイレクト (redirect) という機能を使って変更できます(リダイレクトの詳細は第45章で取り上げます)。そのため、標準出力と標準エラーを使い分けるようにプログラムを作っておけば、エラーメッセージだけを別の場所に書き出して取っておくとか、正常時のメッセージが大量にあるときに、重大なエラーメッセージが埋もれないようにするといったことが可能になります。


標準エラーへ出力するには、これまでに使っていた出力関数(puts関数printf関数)に代わって、fputs関数fprintf関数を使います。

fputs関数

fputs関数は、第1引数に出力したい文字列を、第2引数に出力先を指定します。fputs関数は、<stdio.h> で宣言されています。

第2引数に stderr を指定すると標準エラーへ出力し、stdout を指定すると標準出力へ出力できます。stderr と stdout は、<stdio.h> で定義されているマクロです。

なお、puts関数と違って、fputs関数は自動的に改行してくれないことに注意してください。

fputs("Hello.\n", stderr);  // 標準エラーへ出力
fputs("Hello.\n", stdout);  // 標準出力へ出力

次のサンプルプログラムは、標準出力と標準エラーの両方を使っています。

#include <stdio.h>

void print_div_answer(int num, int d);

int main(void)
{
    print_div_answer(100, 10);
    print_div_answer(100, 0);
    print_div_answer(100, 5);
}

void print_div_answer(int num, int d)
{
    if (d == 0) {
        fputs("Divide by zero!\n", stderr);
    }
    else {
        printf("%d\n", num / d);
    }
}

実行結果:

10
Divide by zero!
20

標準出力と標準エラーが、同じ場所(画面など)になっている環境では、両方の出力が混ざります。

【上級】バッファリングの設定次第で結果の混ざり方は変わります。たとえば、標準出力がフルバッファリング、標準エラーがバッファリング無しになっていたら、実行結果は「Divide by zero!、10、20」の順番に並ぶでしょう(第43章

fprintf関数

fprintf関数は、第1引数に出力先を指定します。第2引数以降は printf関数と同じで、変換指定と、対応する引数の並びが続きます。出力先の指定以外は printf関数と同じと考えて構いません。fprintf関数は、<stdio.h> で宣言されています。

fprintf(stderr, "%d %f\n", a, b);  // 標準エラーへ出力
fprintf(stdout, "%d %f\n", a, b);  // 標準出力へ出力

アサート

アサート(アサーション、表明) (assert、assertion) は、プログラム内のある時点で想定される状況(条件)を、ソースコード上に記述するものです。たとえば、「この処理を終えたとき、変数v の値は 0~100 の範囲に収まっているはずだ」とか「この関数の引数に、負数が渡されてくることはない(そういう仕様であることを利用者に伝えているので、それを破るのは利用者のミスである)」といったものです。

こうした想定を if文で記述し、想定通りの状況になっていなければ、エラーメッセージを出力するといったことは、これまでの知識でも可能ですが、アサートはマクロによってこれを実現します。次のコードは、仮引数v に渡される値が 0以上であることを想定している関数の例です。

// if文の場合(ここまでの知識だけで実現すると)
bool f(int v)
{
    if (v < 0) {
        fputs("v must be a positive number.\n", stderr);
        return false;
    }

    //...

    return true;
}

// アサートの場合
void f(int v)
{
    assert(v >= 0);

    //...
}

assert(v >= 0); がアサートの記述例です。v >= 0 が想定している状況を表した条件式で、これが false になった場合に、それを検知して決められた動作を取ります。if文で書いた場合とは、条件式が逆になっていることに注意してください。アサートの条件式は「想定している状況」を書き表す(表明する)ものだからです。

if文の場合よりもアサートの方がすっきりしており、分岐のコードがなくなっています。分岐のコードがあると、正常な流れと異常な流れに分かれることになり、ソースコードを把握する労力が増します。アサートは、それがアサートであることが明白で、ソースコードの可読性を高めることにも貢献しているといえます。

このあと説明していきますが、アサートの場合には、想定している状況どおりでなかったとき、通常はプログラムの実行を終了させます。想定どおりの状況でないことが発覚したのなら、それ以降の処理を続行することは危険な行為であるため、一刻も早くプログラムの実行を止めるべきという考え方です。

一方、if文を使った例では、想定どおりでない状況を検出したとき、戻り値として false を返すことによって、呼び出し元に「何かおかしなことが起きている」と伝えようとしています(if文を使いつつ、プログラムを強制終了させる方法もあります)。戻り値を使う場合、関数を呼び出した側が戻り値をチェックして、適切に処置を行わなければなりませんが、戻り値は無視できてしまうため(第9章)、想定外の状況を無視して続行し、より悪い状況に突き進んでしまう恐れがあります。

アサートと、エラーを返す方法とを使い分ける基準の1つは、エラーから復帰できる可能性があるかどうかです。アサートでは、プログラムの実行は止めることが原則であり、復帰はしません。

assertマクロ

さきほどのサンプルプログラムで使った assert は、assertマクロという標準ライブラリで定義されている関数形式マクロで、<assert.h> という標準ヘッダにあります。

assertマクロの実引数には条件式を記述します。評価結果が true になる場合は何も行いませんが、false になる場合には以下のことを行います。

  1. 以下の内容を標準エラーへ出力する(形式は処理系定義)
    • 条件式を文字列化したもの
    • __FILE__、__LINE__、__func__ の結果
  2. abort関数を呼び出して、プログラムを異常終了させる

【C89/95 経験者】C89/95 には __func__ がなかったので、この結果は出力されません。

まず、エラーメッセージを出力します。その形式は処理系定義ですが、少なくとも、どのような条件式であったかと、 __FILE__、__LINE__、__func__ の値が含まれます(これらの意味は第29章を参照)。そのため、プログラム内のどのソースファイルのどの位置でのアサートに引っかかったのかが明確に分かります。

その後、abort関数を呼び出して、プログラムの実行を強制的に終了(異常終了)させます。abort関数については後で取り上げます

実際に、assertマクロで停止したときにどうなるか体験しておきましょう。次のプログラムは、変数v の値が 100 より大きくなければ停止します。

#include <assert.h>

int main(void)
{
    int v = 10;
    assert(v > 100);
}

Visual Studio 2015 で実行すると、コマンドプロンプトに次のように出力されます。

Assertion failed: v > 100, file c:\test_program\main.c, line 6

冒頭の “Assertion failed:” が assert で停止したことを示しており、その後ろに、記述した条件式、__FILE__、__LINE__ の結果が出力されています。

Visual Studio 2015 では __func__ の情報が出力されないようです(2017、2019、2022 でも同様)。

そして abort関数による異常終了の結果として、ダイアログボックスが出現します。ここには、そのあとどうするかを指定する3つのボタン(中止、再試行、無視)があり、【中止】を選ぶと終了、【再試行】を選ぶとデバッグ作業を開始できます。詳細は、「Visual Studio編>異常終了とデバッグ」を参照してください。


assertマクロはたとえば次のように定義されており、NDEBUGマクロの定義の有無によって、置換結果が変化します。

#if defined(NDEBUG)
#define assert(expr) ((void)0)
#else
#define assert(expr) 処理系定義のコード
#endif

(void)0 は、void式(第27章)なので、特に何もしないということです。したがって、NDEBUGマクロが定義されている場合、assertマクロは何も行わないようになります

assertマクロは標準ヘッダの側に定義されていますが、NDEBUGマクロを定義する(あるいはしない)のは我々の仕事です。NDEBUGマクロを定義する場合、assertマクロの定義のところからみえなければいけないので、<assert.h> をインクルードするよりも前で定義します。

ただし、Visual Studio では、ビルド構成を変えることによって、自動的に NDEBUGマクロの定義の有無を切り替えるようになっています。Debugビルドでは NDEBUGマクロは定義されず、Releaseビルドでは定義されます。このため、ソースコード上に NDEBUGマクロの定義を記述する必要はありません。ビルド構成については、あとで取り上げます

#define NDEBUG
#include <assert.h>
#include <stdio.h>

int main(void)
{
    char s[40];
    fgets(s, sizeof(s), stdin);
    int value;
    sscanf(s, "%d", &value);

    assert(value != 0);  // ((void)0) に置換されている

    printf("%d\n", 1000 / value);
}

NDEBUGマクロによって assertマクロが機能するかしないかを選択できるので、多くの場合、開発中は NDEBUGマクロを定義しないようにして、assertマクロが機能するようにします。そして、製品として公開・出荷するバージョンでは NDEBUGマクロを定義して、assertマクロが機能しないようにします。assertマクロによるチェックも、結局は if文を実行しているのと変わりはないので、実行時のコストになりますし、実行ファイルのサイズも増加するため、製品版ではその余分なコストを消すことが好まれるためです。しかし、製品版にバグが残っていることもあるので(むしろ普通なことです)、あえて製品版でも assert を有効なままにしておく場合もあります。ユーザーの環境で、assert による謎のエラーメッセージが表示されてしまうかもしれませんが、その内容を報告してもらえれば修正の助けになります。

assertマクロの機能の有無によってプログラムには違いが生じているので、切り替えを行うのなら、最終出荷バージョンはあらためて入念に動作確認しなければなりません。

assertマクロの実引数に記述する内容には注意しなければならないことがあります。assertマクロが機能しないようになったとき、実引数の式は評価されません。そのため、次のような使い方は問題になります。

bool f(int v);

int value = 100;
assert(f(value));

f関数は、何かしらの処理を行った結果、正常に終了したなら true を、正常に終了できなかったのなら false を返すとします。この成否をチェックするために、assertマクロを使っています。assertマクロが機能しているときは何ら問題はないですが、機能しないとき(NDEBUGマクロを定義したとき)には、assert(f(value)) はごっそりと空のコードに置き換わりますから、f関数の呼び出しごと消えてなくなります。f関数の呼び出し自体は必要なものであるはずなので、これは不適切でしょう。

この例では、f関数の呼び出しを assert から切り離さなければなりません。

bool f(int v);

int value = 100;
bool result = f(value);
assert(result);

しかし今度は、assertマクロの置換結果が空になる場合に、変数result の値が使われていないことに対する警告が出るかもしれません。

ビルド構成と最適化

NDEBUGマクロを定義するかどうかは、開発中なのでデバッグ機能を有効にするか、本番に向けた完成版なのでデバッグ機能を無効にするか、といった違いを意味しています。これは、2通りのビルドを使い分けるということであり、Visual Studio ではビルド構成として用意されています。ビルド構成を変更する方法については、「Visual Studio編>ビルド構成について」のページを参照してください。

また、プログラムを最適化 (optimize) するかどうかも重要なポイントです。ここでの最適化は、コンパイラやリンカが持つ機能のことで、ソースコードの意味を変えることなく、より効率よく動作するようにうまく変形することをいいます。効率良く動作することは望ましいことなので、本番用のビルドでは有効にしたいですが、どのような変形が施されたかを把握することが難しいため、デバッグがやりにくくなる問題があります。そのため、開発中には最適化を行わないことが多いです。

たとえば、最適化されたコードをステップ実行(「Visual Studio編>ステップ実行」を参照)すると、ソースファイルの記述通りに進行しないことが分かります。

組み合わせとして、以下の4つが考えられます。

  1. デバッグ機能は有効で、最適化はしない
  2. デバッグ機能は有効で、最適化を行う
  3. デバッグ機能は無効で、最適化はしない
  4. デバッグ機能は無効で、最適化を行う

Visual Studio の Debugビルド構成は1番にあたり、Releaseビルド構成は4番にあたります。

2番はそれなりの価値があります。前述したとおり、最適化されるとデバッグがやりにくくなるため、デバッグ機能を有効なままにしておくと助けになります。また、取り切れなかったバグがユーザーの元で発生してしまう可能性を考慮して、あえていくつかのデバッグ機能を残したままリリースすると、問題の原因を突き止めやすくなり、修正版を作る助けになるかもしれません(悪用されないように注意が必要です)。

実行を終了させる

assertマクロは、abort関数を使って実行を終了させています。このように、main関数のコードが終了する以外に、プログラムの実行を終了させる方法がいくつか存在します。

異常終了 (abort関数)

abort関数は、<stdlib.h> で宣言されている関数で、プログラムの実行を異常終了させます。異常終了とはその名の通り、異常な事態が起きているため、プログラムの実行を終了させるということです。

abort関数には引数はなく、関数内で強制終了するため、呼び出し元に戻ってくることもなく、戻り値もありません。

#include <stdlib.h>
#include <stdio.h>

int main(void)
{
    puts("xxx");
    abort();
    puts("yyy");  // 実行されない
}

実行結果:

xxx
(ここで異常終了)

実行結果には xxx が出力されていますが、これは保証されません。abort関数によって終了した場合に、バッファリングされているストリームに溜まっていたデータが出力されるかどうかは処理系定義とされているためです2。確実に出力させたいものは、標準エラーのようにバッファリングされていないストリームへ出力するか、明示的なフラッシュ操作(第43章)を行うようにします。

【上級】ストリームがクローズされるかどうか、一時ファイルが削除されるかどうかについても処理系定義です2

異常終了は、あくまでも「異常」なことが起きているときに行うものであって、「正常」な流れで実行を終了させたいときに abort関数を使ってはいけません。エラーが起きたから実行を止めたいというケースでも、そのエラーがあらかじめ想定できるものであるなら、正常な終了と捉えます。たとえば、標準入力から入力される内容の不備は、当然起こり得ることです。

正常な強制終了には、このあと説明する exit関数などを使います。

正常終了(exit関数)

想定内のエラーの発生によってプログラムの実行を終了させる場合、exit関数を使います。<stdlib.h> で宣言されています。

exit関数には int型の引数が1つあり、渡した値が、プログラムを呼び出す側へ返されます。つまり、main関数の return文で記述する値と同じ扱いになります。

終了させる意味が、成功による場合には 0EXIT_SUCCESS、失敗による場合には EXIT_FAILURE を渡します。EXIT_SUCCESS と EXIT_FAILURE は標準ライブラリで定義されているマクロです。

#include <stdlib.h>
#include <stdio.h>

int main(void)
{
    puts("xxx");
    exit(0);
    puts("yyy");  // 実行されない
}

実行結果:

xxx
(ここで終了)

abort関数の場合と違い、終了時に異常を示すメッセージが出るといったこともなく、穏やかに終了します。

exit関数による終了では、バッファリングによって溜まっていたデータは出力されます。3

【上級】ストリームのクローズ、一時ファイルの削除についても保証されます3

【上級】ストリームのフラッシュやクローズ、一時ファイルの削除を行わず、いち早くプログラムを終了させることができる _Exit関数も存在します。行われなかった後始末は、プログラムの実行終了後に OS が行ってくれることを期待しています。

【C11】【上級】さらに、quick_exit関数が追加されました。_Exit関数と違うのは、終了前に呼び出す関数を登録しておける点です。

独自のアサート

自分で都合の良い仕様のアサートマクロを作ることも可能ですし、実際、よく行われています。

標準の assertマクロで出力される内容は、条件式、ファイル名、行数の3つの情報ですが、ここにさらに自分の好きなメッセージを追加できるようにしてみましょう。そのためには、次のように定義します。

#ifdef NDEBUG
#define myassert(b, str)
#else
#define myassert(b, str) \
    if (!(b)) { \
        fprintf(stderr, "assert: %s \n%s\n%s Line:%d (%s)\n", #b, str, __FILE__, __LINE__, __func__); \
        abort(); \
    }
#endif

この myassertマクロは、2つの引数をとります。1つ目は、標準の assert と同じく条件式で、これが偽になったときに停止します。2つ目の引数は、停止した場合に、標準エラーへ出力する文字列です。

さて、この myassertマクロを使って、先ほどのプログラム例を書き換えるとこうなります。

#include <stdio.h>
#include <stdlib.h>

#ifdef NDEBUG
#define myassert(b, str)
#else
#define myassert(b, str) \
    if (!(b)) { \
        fprintf(stderr, "assert: %s \n%s\n%s Line:%d (%s)\n", #b, str, __FILE__, __LINE__, __func__); \
        abort(); \
    }
#endif

int divide(int num, int d);

int main(void)
{
    printf("%d\n", divide(100, 0));
}

int divide(int num, int d)
{
    myassert(d != 0, "Divide by zero!");

    return num / d;
}

実行結果:

assert: d != 0
Divide by zero!
c:\main.c Line:23 (divide)

実行結果にあるように、実引数で指定したメッセージが、標準エラーに出力されるので、停止した理由を分かりやすく示せます。

今回の例は、非常に単純な拡張ですが、単なる文字列ではなく printf関数の形式で文字列を渡せるようにしたり、すぐに停止させてしまうのではなく、停止してもいいか確認してから停止させるようにしたりできます。停止させないことを選択すると、そのアサートを無視して実行を続行させます。

たまに、真のときに停止するアサートや、そもそも条件式のないアサート(必ず停止する)を見かけますが、アサートは「正しい状態を表明する」というニュアンスがあるものなので、これではアサートとは呼べません。そういうマクロを作っていけないわけではありませんが、その場合、アサートという名前を付けないほうがいいでしょう。

コンパイル時アサート

assertマクロは、プログラムの実行中の想定を記述したものでした。同じ考え方をコンパイル時の想定を記述することに置き換えると、コンパイル時アサート (compile-time assert) や静的アサート (static assert) になります。つまり、コンパイルの時点で判断がつくような想定であれば、その想定がおかしい場合にコンパイルの続行を止めます。

次のマクロは、コンパイル時アサートを実現する方法の1つです。

#define STATIC_ASSERT(exp)   typedef char static_assert_dummy[exp ? 1 : -1]

このマクロは、問題があるコードをコンパイル時に検出して、コンパイルエラーを強制的に起こします。

コンパイル時に問題を検出できなければならないので、コンパイル時アサートに与える式は定数式(第10章)でなければなりません

【C++プログラマー】C言語では C99規格以前には、標準のコンパイル時アサートが存在しないため、このようなマクロを自分で用意する必要があります。C11規格になって、標準のコンパイル時アサートが追加されています。

この STATIC_ASSERTマクロの仕組みはなかなか技巧的です。マクロ内でダミーの配列型を typedef で定義しています。その際の要素数の指定の仕方がポイントで、条件式が真であれば 1、偽であれば -1 としています。要素数に負数を指定した配列は宣言できないため、条件式が偽のときにだけコンパイルエラーが起こるという仕組みです。

コンパイル時アサートを使う事例として、型の大きさが想定どおりかどうかを調べるだとか、配列の要素数がきちんと指定されているかを調べることなどがあります。たとえば、typedef を次のように使ったとします。

typedef int int32;

これは、int型の大きさが 32ビット (4バイト) であることを想定したものです。しかし、この想定が真ではない環境でこの型を使ってしまったら、間違った結果を生んでしまうことでしょう。このような事故を、コンパイル時アサートを使って防げます。

#define STATIC_ASSERT(exp)   typedef char static_assert_dummy[exp ? 1 : -1]

typedef int int32;
STATIC_ASSERT(sizeof(int32) == 4);

int main(void)
{
}

sizeof(int32) == 4 が真になる環境では、このプログラムは問題なくコンパイルできます。一方、偽になる環境では、次のようなコンパイルエラーが出力されます。

main.c(4): error C2118: 添字が負の数です。

エラーの文面が、本当のエラーの内容を表すものにならないのが惜しいところですが、少なくともソースファイル上での問題の位置は分かるので、そこを見れば起きていることが分かるはずです。

【C11】標準のコンパイル時アサートとして、_Static_assert が追加されました。これは標準ライブラリで実装されたものではなく、言語自体の機能になっており、ヘッダのインクルードも不要です。また、<assert.h> をインクルードすると、static_assert という名前で使用できます。


練習問題

問題① 標準エラーに、末尾での改行付きで文字列を出力するマクロを、デバッグ作業中にだけ有効になるように作成してください。

問題② assert の次のような使い方には問題があります。理由を説明して、正しく修正してください。

int n = -2;
assert(++n >= 0);

問題③ 以下の中から、コンパイル時アサートで記述できる想定はどれか、すべて選んでください。

  1. short型の大きさが 2バイト以上である
  2. int型と long型の大きさが同じである
  3. 標準入力から入力された整数が 0 でない
  4. const を付加した変数 X の値が 1000以上である
  5. NDEBUGマクロが定義されている


解答ページはこちら

参考リンク


更新履歴

≪さらに古い更新履歴を展開する≫



前の章へ (第29章 事前定義マクロとプラグマ)

次の章へ (第31章 ポインタ①(概要))

C言語編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る