C++で、実行中に作成した文字列をassertで出力する方法


※本記事では、「ファイルを読み込み、成否をbool型で返すLoadFile関数」があると仮定して解説する。

assertの概要

(Visual)C++のassertマクロは、エラー箇所で自動的に実行を停止することができる、便利なデバッグ用機能だ。

assert関数のもっともシンプルな利用例
// ※LoadFile関数は、ファイルを読み込み、成否をbool型で返す関数とする
bool loadResult = LoadFile("config.txt");
// assertマクロ関数は、引数がtrueだったときのみ動作する
assert(loadResult);

//---------------------------------------
// assert出力結果抜粋:LoadFile(loadResult)

上記のソースではエラーメッセージとして分かりづらい。
そこで、assertの動作条件がtrueかfalseかであることを利用して、このような使い方もできる。

assertで任意の文字列を表示する
// ※LoadFile関数は、ファイルを読み込み、成否をbool型で返す関数とする
if (!LoadFile("config.txt"))
{
    // assertマクロ関数は、引数がtrueだったときのみ動作する
    assert(0 && "設定ファイル読み込み失敗");
}

//---------------------------------------
// assert出力結果抜粋:0 && "設定ファイル読み込み失敗"

実行中に作成した文字列をassertできない

さて、ここで本題である。
今回の例の場合、「読み込みに失敗したファイル名」をassert画面で表示したいときがあるだろう。
とりあえず文字列を渡しておけば良さそうに見えるので、思った通りに書いてみる。

うまくいかない例
// ※LoadFile関数は、ファイルを読み込み、成否をbool型で返す関数とする
std::string filename = "config.txt";
if (!LoadFile(filename))
{
    std::string errorMsg = "読み込み失敗:" + filename;
    assert(0 && errorMsg.c_str());
}

//---------------------------------------
// assert出力結果抜粋:0 && errorMsg.c_str()

errorMsg.c_str()の戻り値が展開されず、そのままソースコードの内容が展開されてしまった。

とりあえず解決方法

先にちゃんと動いてくれる例を提示しておく。
assertではなく、_wassertを使うことになる。
結論としては、使いやすい自作assert作るしかないんですね……

実行中に作成した文字列をassertで出力する
// ※LoadFile関数は、ファイルを読み込み、成否をbool型で返す関数とする
std::string filename = "config.txt";
if (!LoadFile(filename))
{
    // エラーメッセージ
    std::string errorMsg = "読み込み失敗:" + filename;

    //ワイド文字列に変換
    WCHAR* _wtext = new WCHAR[strlen(errorMsg.c_str()) + 1];
    mbstowcs_s(nullptr, _wtext, strlen(errorMsg.c_str()) + 1, errorMsg.c_str(), _TRUNCATE);

    // _wassertを使って、文字列出力
    // (現在のファイルや行数は__FILE__マクロと__LINE__マクロで取得可能)
    _wassert(_wtext, _CRT_WIDE(__FILE__), (unsigned)(__LINE__));

    // ワイド文字列を解放
    delete[] _wtext;
}

//---------------------------------------
// assert出力結果抜粋:読み込み失敗:config.txt

超絶簡易な解説

以下、上記のソースの簡易な解説。

assertマクロの中身

まずは、assertマクロが何故このような挙動をするのか、中身を見て確認してみよう。

assertマクロのソース
#define assert(expression) (void)(                                                       \
        (!!(expression)) ||                                                              \
        (_wassert(_CRT_WIDE(#expression), _CRT_WIDE(__FILE__), (unsigned)(__LINE__)), 0) \
    )

ここで着目したい点は、以下の4点。

  • 本質的には「_wassert」関数を使ってassertを出力していること。
  • _wassert関数の各引数の文字列は、「_CRT_WIDE」マクロ関数でワイド文字列に変換してから渡していること。
  • assertマクロの引数expressionが、「#」でソースコード上の記述そのままで出すようになっていること。
  • 該当する箇所のファイル名と行数を、マクロ「__FILE__」「__LINE__」で取得していること。

これを踏まえると、「自力で_wassert関数を呼び出す。ただし、文字列はワイド文字列に変換する必要がある。」ということだ。

ただし、ワイド文字列に変換する「_CRT_WIDE」マクロ関数は、本質的には「文字列リテラルをの先頭に「L」を追加しているだけ」である。
つまり、「_CRT_WIDE(errorMsg.c_str())」 のようにシンボル名を入れてしまうと「LerrorMsg.c_str()」としてコンパイルされることになり、コンパイルエラーが発生する。
すなわち、自分でワイド文字列に変換する処理を書いてあげる必要がある
今回はmbstowcs_s関数を使用した。

結論

つまり、自作assert作れってことですね、ハイ。