アサート | Programming Place Plus 新C++編

トップページ新C++編

このページの概要

このページでは、アサートについて取り上げます。アサートは、満たされるべき条件をソースコードの形で記述しておくことで、その条件が満たされてないという想定外の状況をいち早く発見する仕組みです。アサートは、プログラムの開発段階でバグを発見するために非常に有用です。そのほか、アサートに関連して、プログラムの実行を強制終了させる方法なども取り上げます。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



アサート

前のページでプリプロセスで行えることを一通り学びました。このページでは、それらの機能を用いて実現できる有用なデバッグ機能を紹介します。それがアサート(アサーション、表明) (assert、assertion) です。

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

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

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

    //...

    return true;
}

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

    //...
}

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

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

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

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

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

assertマクロ

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

【C言語プログラマー】C言語の assertマクロと同じものであると考えて問題ありません。

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

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

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

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

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

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

#include <cassert>

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

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

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

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

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

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


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

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

(void)0 は、C言語から受け継いだキャストの構文で、0 を void にキャストしています。void にキャストすることは、式ではあるが、その値は捨てるという意味です。(void)0 は何もしない式ということになります。したがって、NDEBUGマクロが定義されている場合、assertマクロは何も行わないようになります

(void) を、使われていない変数があるという警告を黙らせるために使うこともあります。たとえば、使われていない変数x に対して (void)x; のように書いておけば、コンパイラは警告を出さなくなる場合が多いです(確実に効果がある保証はありません)。

【C++17】上のコラムの用途を、[[maybe_unused]]属性4で行えるようになりました。

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

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

#define NDEBUG
#include <cassert>
#include <iostream>

int main()
{
    int value {};
    std::cin >> value;
    assert(value != 0);  // ((void)0) に置換されている

    std::cout << 1000 / value << "\n";
}

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

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

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

bool get_string(std::string& s);

std::string s {};
assert(get_string(s));
std::cout << s << "\n";

get_string関数は、実引数を参照として受け取り、呼び出し元の変数に結果を取得するようになっており、戻り値は処理の成否を返すとします。get_string が成功したかどうかをチェックするために assertマクロを使っています。assertマクロが機能しているときは何ら問題はないですが、機能しないとき(NDEBUGマクロを定義したとき)には、assert(get_string(s)) はごっそりと空のコードに置き換わりますから、get_string関数の呼び出しごと消えてなくなります。get_string関数の呼び出し自体は必要なものであるはずなので、これは不適切でしょう。

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

bool get_string(std::string& s);

std::string s {};
bool result {get_string(s)};
assert(result);
std::cout << s << "\n";

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

【C++17】[[maybe_unused]]属性4を使えば、この警告も抑止できます。

ビルド構成と最適化

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

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

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

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

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

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

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

実行を終了させる

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

異常終了 (std::abort関数)

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

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

#include <cstdlib>
#include <iostream>

int main()
{
    std::cout << "xxx\n";
    std::abort();
    std::cout << "yyy\n";  // 実行されない
}

実行結果:

xxx
(ここで異常終了)

実行結果には xxx が出力されていますが、これは保証されません。std::abort関数によって終了した場合に、バッファリングされているストリームがフラッシュされるかどうか(「ファイルとエラー」のページを参照)、ストリームがクローズされるかどうかは処理系定義です7。確実に出力させたいものは標準エラーのようにバッファリングされていないストリームへ出力するか、明示的にフラッシュしてください。

【上級】生存していたオブジェクトに対するデストラクタは呼び出されません。また、std::atexit関数で登録した関数は呼び出されません。8

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

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

正常終了(std::quick_exit関数)

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

std::quick_exit関数には int型の引数が1つあり、渡した値が、プログラムを呼び出す側へ返されます。つまり、main関数の return文で記述する値と同じ扱いになるので、成功を意味するときは 0EXIT_SUCCESS、失敗を意味するときは EXIT_FAILURE を渡します(「ファイルとエラー」のページを参照)。

#include <cstdlib>
#include <iostream>

int main()
{
    std::cout << "xxx\n";
    std::quick_exit(0);
    std::cout << "yyy\n";  // 実行されない
}

実行結果:

xxx
(ここで終了)

実行結果には xxx が出力されていますが、これは保証されません。std::quick_exit関数によって終了した場合、標準ストリームのバッファはフラッシュされません11そのほか開かれていたストリームについて、フラッシュやクローズが行われるかどうかは処理系定義です12。確実に出力させたいものは標準エラーのようにバッファリングされていないストリームへ出力するか、明示的にフラッシュしてください。

【上級】生存していたオブジェクトに対するデストラクタは呼び出されません。また、std::atexit関数で登録した関数は呼び出されませんが、std::at_quick_exit関数で登録された関数が呼び出されます。13

std::quick_exit関数は、本来必要ないくつかの後始末をスキップして終了するわけですが、これはいち早く実行を終了させようとしているからであり、“quick_exit” という名前なのはこのためです。行われなかった後始末は、実行終了後に OS が行ってくれることを期待しています。

後始末をきちんと行う std::exit関数14 15も存在しますが、通常、std::quick_exit関数を使ったほうが良いです。プログラムの作りによっては、std::exit関数では実行を終了できず、プログラムがフリーズした状態に陥ることがあります。

【上級】たとえば、std::exit関数はデストラクタを呼ぶので、デストラクタの中でスレッドの終了などを待機するコードがあると、いつまでも終了待ちを続けてしまう可能性があり、プログラムの実行を終えられません。

帰らない関数

std::abort関数や std::quick_exit関数のように、関数内から呼び出し元へ帰ってくることがない特殊な関数には、それを明示する属性 (attribute) が付加されています。それぞれの関数の宣言は次のようになっています。

[[noreturn]] void abort(void) noexcept;
[[noreturn]] void quick_exit(int status) noexcept;

noexcept は今のところは無視して構いません。

【上級】noexcept は、関数から例外が送出されないことを明示したものです。

[[noreturn]] は、[[noreturn]]属性と呼ばれ、この関数からは帰らないことを意味しています。[[noreturn]]属性が記述されていると、コンパイラがコードを最適化することに役立つほか、関数から return する経路が存在しないという主旨の警告を抑制する効果があります。どんな経路を通っても必ず std::abort関数や std::quick_exit関数が呼び出されるような関数を自作する場合、その関数の宣言に [[noreturn]]属性を付加すると良いです。

#include <cstdlib>

[[noreturn]] void exit_program()
{
    std::quick_exit(0);
}

int main()
{
    exit_program();
}

実行結果:

static_assert

assertマクロは、プログラムの実行中の想定を記述したものでした。同じ考え方で、コンパイル時の想定を記述するものが static_assert です。assertマクロを実行時アサート (runtime assert)、static_assert をコンパイル時アサート (compile-time assert) と呼ぶことがあります。

static_assert はマクロではなく、言語の文法の一部として用意されています。次の構文で記述します。

static_assert(条件式, メッセージ);

【C++17】「メッセージ」を省略できるようになりました。

コンパイル時に判定されるので、「条件式」は定数式でなければならず、評価した結果が bool型に変換できるものでなければなりません。結果が true の場合は何も起こらず、false の場合にはコンパイル作業が中止されて、「メッセージ」の内容を含んだ文字列が、コンパイル結果が表示される場所へ出力されます。

「メッセージ」は文字列リテラルで記述します。基本ソース文字セット(「文字」のページを参照)に含まれない文字が出力できるかどうかは処理系定義です。

static_assert の評価はコンパイル時に起こるので、プログラムの実行の流れとは関係がなく、比較的自由にどこにでも書けます。関数定義の内外のほか、構造体定義の内側などでも記述できます。

利用例の1つとして、型の大きさが想定どおりであるか調べるというものがあります。

static_assert(sizeof(long) == 8, "long type is not 8 bytes.");

int main()
{
}

long型の大きさが 8バイトの処理系では、このプログラムは問題なくコンパイルされます。8バイトでない処理系では、static_assert が失敗して、たとえば次のような結果が出力されます。

1>c:\test_program\main.cpp(1): error C2338: long type is not 8 bytes.

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

assertマクロを次のように使うことには問題があります。理由を説明して、正しく修正してください。

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

解答・解説

問題2 (確認★)

以下の中から、static_assert で記述できる想定はどれか、すべて選んでください。

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

解答・解説

問題3 (応用★★)

ポーカープログラムの judge_poker_hand関数は、引数で渡されてくるカード情報が、5枚分であることや、適切に整列済みであることを求めています。現状、if文でチェックしていますが、アサートで置き換えてください。

現在のポーカープログラムの最終形は、「プリプロセス」のページの練習問題の解答にあります。

解答・解説

問題4 (応用★★★)

assertマクロで停止したときに出力されるメッセージに加えて、任意のメッセージも出力できるようなアサートマクロを作ってください。たとえば次のような使い方ができるようにします。

my_assert(v >= 0, "v must not be negative");

解答・解説


解答・解説ページの先頭



更新履歴




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