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

C言語編 第30章 アサート

先頭へ戻る

この章の概要

この章の概要です。


標準エラー

ここまでの章では、何らかの出力を行うときには、標準出力に対して行っていました。実はこれ以外に、標準で使うことができる出力先があります。それが、標準エラーです。

標準エラーは標準出力と同様、具体的に何であるかは環境によって異なりますが、やはり画面であることが多いです。

標準エラーは、何らかのエラーや問題が発生した際に、その情報を出力するために使われます。標準出力と標準エラーには動作上の違いがあって、標準出力では、出力処理がただちに実行されないことがあります。これは、ある程度のデータが集まってから、まとめて実行するという方式を採ることがあるからです。

このような仕組みを、バッファリングといいます。第43章で扱います。

これに対して、標準エラーは、出力処理がただちに実行されます。そのため、万が一、プログラムの実行中に問題が起きて、強制終了せねばならない事態に陥ったとしても、それまでに出力しようとしていたものは、きちんと出力済みになっていることが保証できます。一般に、エラーに関する情報は失われてはならない(無視されては困る)ので、標準エラーを使って出力した方が良いということになります。

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

fputs関数は、第1引数に出力したい文字列を、第2引数に出力先を指定します。

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

本来、fputs関数の第2引数には FILE*型で表される、ストリームを指定します(第39章

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

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

fprintf関数の方は、第1引数に出力先を指定します。第2引数以降は、printf関数と同じで、変換指定と、対応する引数の並びが続きます。こちらは、出力先の指定以外は printf関数と同じと考えて構いません。

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

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

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

void printDivAnswer(int num, int d);

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

    return 0;
}

void printDivAnswer(int num, int d)
{
    if( d == 0 ){
        fputs( "0 で除算することはできない。\n", stderr );
    }
    else{
        printf( "%d\n", num / d );
    }
}

実行結果:

10
0 で除算することはできない。
20

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

バッファリングの設定次第で結果の混ざり方は変わります。例えば、標準出力がフルバッファリング、標準エラーがバッファリング無しになっていたら、先ほどのサンプルプログラムの実行結果は「0 で除算することはできない。、10、20」の順番に並ぶでしょう(第43章)。


アサートマクロ

assert というマクロは、デバッグの助けとして非常に有益です。assert は「アサート」と読み、「表明する」という意味です。

assert は、<assert.h> に定義されています。

アサートは、プログラム内のある箇所で、「処理がこの位置に到達したとき、こうなっていなければならない」という予定を表明します。プログラムを実行したとき、もし、予定通りの状態になっていなければ、プログラムの実行をその場で停止して、標準エラーにメッセージを出力します。

どのように停止するのか確認しておいた方が良いでしょう。実際に試してみます。

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

int divide(int num, int d);

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

    return 0;
}

int divide(int num, int d)
{
    assert( d != 0 );  /* 0 で除算することはできない */

    return num / d;
}

実行結果:

Assertion failed: d != 0, file c:\main.c, line 15

assert の実引数が真であれば何も起きません。偽であれば、プログラムは abort関数によって強制的に停止されます。このサンプルプログラムは、偽になるため、すぐに停止します。

abort関数は、<stdlib.h> で宣言されている標準ライブラリ関数で、プログラムを異常終了させるものです。異常終了という言葉が示しているように、これは異常時の強制終了を行うための関数です。

異常時ではなく、普通にプログラムを終了させることが目的であれば、普通に main関数から return するか、exit関数を使います。

なお、assert による停止時には、ソースファイルの名前や行数、どんな条件式が与えられていたかといった情報が、標準エラーへ出力されます。

C99 からは、assert を記述した関数の名前も出力されます。

また、assert は、NDEBUG というオブジェクト形式マクロが定義されていない場合にだけ有効になります

具体的には、次のような感じになっています。

#ifdef NDEBUG
#define assert(b)
#else
#define assert(b) 実装コード
#endif

このような作りなので、NDEBUG を定義するのなら、assert.h をインクルードするよりも前で行う必要があります。

#define NDEBUG
#include <assert.h>

NDEBUG は、VisualStudio を使っているのなら、ビルド構成を変更すれば自動的に定義されます。VisualStudio なら、メニューバーの「ビルド」→「構成マネージャ」をクリックし、「アクティブ ソリューション構成」を「Release」に変更します。デフォルトでは「Debug」になっていると思いますが、これは開発中の設定です。「Release」に変更することは、完成品としてビルドすることを意味しています。

NDEBUG を、デバッグ作業を行っている開発段階では定義せず、完成品をビルドするときに定義するようにすれば、デバッグ作業を行っている段階では assert は有効になり、完成品では無効(空定義)にしておくことができます。あえて完成品で無効にすることには、無駄な処理をなくして性能を向上させる意味があります。

アサートを使うにあたって注意しなければならないことがあります。例えば、次のような使い方には問題があります。

assert( func() != 0 );

この assert は、func関数の戻り値が 0以外であることを想定したものです。

問題は、assert に与えた式は評価(第27章)されるということです。評価されるので、func関数が実際に呼び出されます。当たり前といえば当たり前のようですが、NDEBUG が定義されると、assert そのものが消えてしまうところに罠があります。

NDEBUG が定義されると、func関数を呼び出す式ごと assert が消失してしまいます。もし、func関数の内部で、副作用(第27章)がある処理をしていたら(静的記憶域期間を持つ変数の値を変更したり、何らかの出力を行ったり)、プログラムの実行結果自体が変わってしまうでしょう。これは避けなければならない事態です。

そこで、式の中で、副作用のある部分を抜き出して、assert の手前で行うようにします。先ほどのケースでは、func関数の呼び出しを assert の手前で独立して行い、戻り値をチェックする部分だけを assert の実引数にします。

ret = func();
assert( ret != 0 );

独自のアサート

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

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

#ifdef NDEBUG
#define myassert(b,str)
#else
#define myassert(b,str)	\
    {if(!(b)){fprintf(stderr, "assert: %s \n%s\n%sの%d行目\n", #b, str, __FILE__, __LINE__); 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の%d行目\n", #b, str, __FILE__, __LINE__); abort(); }}
#endif

int divide(int num, int d);

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

    return 0;
}

int divide(int num, int d)
{
    myassert( d != 0, "0 で除算することはできない" );

    return num / d;
}

実行結果:

assert: d != 0
0 で除算することはできない
c:\main.cの21行目

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

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

たまに、真のときに停止するアサートを独自で作っている人や、そもそも条件式のないものを作っている人(必ず停止する)がいますが、それはもはやアサートとは呼べません。そういうマクロを作っていけない訳ではありませんが、その場合、アサートという名前を付けない方が良いです。

コンパイル時アサート

アサートの応用として、コンパイル時アサート(静的アサート)というものがあります。次のマクロは、コンパイル時アサートを実現したものです。

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

このマクロは、問題があるコードをコンパイルの時点で検出して、コンパイルエラーを強制的に起こします。assert はプログラムの実行時に問題を検出するので、検出するタイミングに違いがあります。

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

STATIC_ASSERTマクロの仕組みはなかなか技巧的です。

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

コンパイル時アサートを使う事例として、型の大きさが想定通りかどうかを調べるだとか、配列の要素数がきちんと指定されているかを調べることなどがあります。例えば、第26章で、次のような 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)
{
    return 0;
}

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

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

エラーの文面が、本当のエラーの内容を表すものになりませんが、少なくともソースファイル上の位置が分かるので、そこを見に行けば起きていることが分かるはずです。

C11 (標準のコンパイル時アサート)

C11 になって、標準のコンパイル時アサートである _Static_assert が追加されました。

int main(void)
{
    _Static_assert(sizeof(int) == 4, "");

    return 0;
}

_Static_assert は標準機能なので、ヘッダのインクルードも必要ありません。第1引数の条件式が偽になるとき、コンパイルが失敗し、第2引数に指定した文字列を含んだエラーメッセージを出力します。

また、<assert.h> をインクルードすると、static_assert という名前で使用できるようになります。

static_assert という名前は、C++ の標準機能として存在しているコンパイル時アサートと同じです。

#include <assert.h>

int main(void)
{
    static_assert(sizeof(int) == 4, "");

    return 0;
}

VisualStudio 2015/2017 のいずれも _Static_assert には対応していません。static_assert については、VisualStudio 2015/2017 のいずれでも使用できますが、これは <assert.h> に含まれているものではなく、拡張機能として存在しているもののようです。


練習問題

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

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

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


解答ページはこちら

参考リンク



更新履歴

'2018/6/4 新規作成。
標準エラーは、第39章から一部移動してきて再構成。
アサートやコンパイル時アサートは、第28章から移動してきて再構成。



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

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

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

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS