【DxLib】DXライブラリでも使用できるAssertマクロ


# はじめに

DxLibでゲームを作っている際に，`assert`を使いたいと思うことがありました．
しかし，DxLibで`assert`を使うとDxLibの終了処理`DxLib_End`を呼ぶ前にプログラムが終了してしまうという問題があるため，そのまま使うことはできません．(`assert`ってなにという人は[こちら](#参考にしたリンク))

「龍神録2ゲームプログラミングの館」ではこれの対策として，アサートを自作されています．
内容としては`ERR`というマクロを定義し，マクロからエラー発生条件の表示と`DxLib_End`を呼ぶ機能を持った関数を呼びだすというものです．関数の中では`while(!ProcessMessage())`を読んでプログラムが終了しないようにされており，`printfDx`でエラー文を表示しています．

https://dixq.net/rp2/06.html

大変参考になる実装であり，私も長らくこれを使わせていただいていました．しかし，使用していく中で以下のような要望が自分の中で出てきました．

- `DxLib_Init`を呼ぶ前でも使用できるようにしたい
- `assert`のようにReleaseビルド時には処理されてほしくない
- 折角なら`__LINE__`や`__FILE__`ではなく，`source_location`を使って呼び出し位置を表示したい

これらを踏まえて作成した，DxLib専用の自作Assertを紹介します．

# ソースコード

ソースコードは以下の通りです．C++20で作成していることに留意してください．

```cpp:dxlib_assert.h
#pragma once

#include <source_location>
#include <stdexcept>
#include <string>

#include <DxLib.h>

// 呼び出さないこと．
namespace assert_internal {

void ErrorAssert(const std::string& conditional_expression, const std::string& error_mes,
                 const std::string& file, const std::string& func, const int line);

}  // namespace assert_internal

#ifdef _DEBUG

#define INTERNAL_ERROR_MESSAGE(expression, error_mes)                       \
const std::source_location location = std::source_location::current();      \
::assert_internal::ErrorAssert(expression, error_mes, location.file_name(), \
    location.function_name(), location.line());

//! @brief エラーが発生したときにエラーメッセージを表示する．
//! DxLib の動作を止め，独自のエラーメッセージを表示する．
//! DxLib が初期化されていない場合は，例外を投げる．
//! @param expr TRUE であることが期待される条件
//! @param error_mes エラーメッセージ
#define ASSERT(expr, error_mes)                                     \
do {                                                                \
    if (!(expr)) {                                                  \
        if (DxLib_IsInit() != TRUE) {                               \
            throw std::runtime_error("DxLib is not initialized.");  \
        } else {                                                    \
            const std::string expr_str = #expr;                     \
            const std::string message = error_mes;                  \
            INTERNAL_ERROR_MESSAGE(expr_str, message);              \
        }                                                           \
    }                                                               \
} while (0)

#define ASSERT_NOT_NULL_PTR(ptr) ASSERT((ptr) != nullptr, "nullptr passed.")
#define ASSERT_MUST_NOT_REACH_HERE() ASSERT(false, "This part is never reached.")

#else

#define ASSERT(expr, error_mes) ((void)0)
#define ASSERT_NOT_NULL_PTR(ptr) ((void)0)
#define ASSERT_MUST_NOT_REACH_HERE() ((void)0)

#endif  // _DEBUG
```

```cpp:﻿dxlib_assert.cpp
#include "dxlib_assert.h"

#include <format>

#include <DxLib.h>

void ::assert_internal::ErrorAssert(
    const std::string& conditional_expression,
    const std::string& error_mes, const std::string& file,
    const std::string& func, const int line) {
    // DxLib がまだ初期化されていない場合は，終了する．
    if (DxLib::DxLib_IsInit() != TRUE) {
        return;
    }

    // エラーメッセージを表示する．
    DxLib::clsDx();
    DxLib::printfDx("Error! Please press the X button on the window to exit! \n\n");
    DxLib::printfDx("%s", std::format("Error Condition : {}\n\n", conditional_expression).c_str());
    DxLib::printfDx("%s", std::format("Error Cause : {}\n\n", error_mes).c_str());
    DxLib::printfDx("%s", std::format("File Name : {}\n\n", file).c_str());
    DxLib::printfDx("%s", std::format("Function Name : {}\n\n", func).c_str());
    DxLib::printfDx("%s", std::format("Line Number : {}\n\n", line).c_str());

    // 何もせずに待つ．
    while (!DxLib::ProcessMessage()) {
        DxLib::ClearDrawScreen();
        DxLib::ScreenFlip();
    }

    DxLib::DxLib_End();

    // プログラムを終了する．
    exit(99);
}
```

# マクロについて

### `ASSERT(expr, error_mes)`

基本的なアサートの処理です．`expr`のほうに`true`となっていてほしい条件式，`error_mes`のほうに表示されるエラーメッセージを渡してください．なお，マクロは全てReleaseビルドの際は`((void)0)`に置き換えられ無視されます．
また，`DxLib_Init`を呼ぶ前に使用すると，例外を出すようになっています．

### `ASSERT_NOT_NULL_PTR(ptr)`

`ptr`を`nullptr`と比較し，ヌルポインタではないことを確認します．個人的にnullチェックはたくさん書くので，専用のマクロを用意してみました．

### `ASSERT_MUST_NOT_REACH_HERE()`

switchの`default`など「ここまで処理が来るはずない」というところで使用します．`ASSERT(false, "This part is never reached.")`に置換されるので，必ずアサートは失敗します．

# 使用例

```cpp:sample.cpp
#include <DxLib.h>

#include "dxlib_assert.h"

void Success() {
    ASSERT(1 == 1, "これは呼ばれません");
}

void Failure() {
    ASSERT(1 == 2, "条件式がfalseなので，アサートが呼ばれます");
}

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
    ChangeWindowMode(TRUE);
    SetGraphMode(800, 600, 32);
    SetMainWindowText("dxlib_assert");

    if (DxLib_Init() == -1) {
        return -1;
    }

    SetDrawScreen(DX_SCREEN_BACK);

    while (!ProcessMessage()) {
        ClearDrawScreen();

        Success();
        Failure();

        ScreenFlip();
    }

    DxLib_End();

    return 0;
}
```

プログラムを実行すると以下のようにアサートが呼ばれます．

![スクリーンショット 2024-10-19 233921.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632687/b605d0e0-412c-96a5-5cde-3c43beeb3244.png)

# 参考にしたリンク

`assert`についてしらない方は，以下のページが参考になると思います．

https://cpprefjp.github.io/reference/cassert/assert.html

<br>

https://dixq.net/rp2/06.html

https://inaenomaki.hatenablog.com/entry/2017/05/04/225746