【C++AdC】std::source_locationで呼び出し元を検知する

に公開

こんにちは、plasmaです。超久々のアドベントカレンダーで緊張しております。

というわけで C++ Advent Calendar 2025 1日目の記事です。

この記事では、趣味で自分用のライブラリを作ってる人向けの話をします。例えば以下のような人です。

  • ゲーム制作が趣味で共通処理などをライブラリ化している
  • 競技プログラミング用のライブラリを整備している
  • 標準ライブラリにもboostにもないライブラリを自作して使っている

逆に業務で使うコードや複数人でメンテしてるOSSなどは対象外です。あくまで趣味グラミング(プログラミングの対)で楽をしたい人のための記事です。

前置き

皆さんは何かしらのライブラリを使用してて、assertに引っかかってることは分かるが呼び出し元が分からない、ということはないでしょうか。自分はよくあります。

例えば「範囲外アクセスを検知するassertに引っかかってるが、どこの呼び出しで範囲外アクセスになってるか分からない」みたいなやつです。

struct my_container {
    auto operator[](int idx) {
        assert(0 <= idx && idx < N);
        // 処理
    }
};

void func2() {
    int idx = 0;
    my_container c;
    while(...) {
        // idxの更新を含む長めの処理
        auto v = c[idx]; // この呼び出しでassertに引っかかるとする
        // ...
    }
}

void func() {
    // ...
    func2();
    // ...
}

int main() {
    // ...
    func();
}

この場合my_container::operator[]がどうのこうのというエラーメッセージが出てプログラムが落ちます。どこで呼び出されたかの情報は乗ってないので不便です。

これが業務だったり人様のライブラリだったりするなら「printfデバッグで探しましょう」とか「gdb叩きましょう」とかになりますが、自分の趣味で書いた自分のためのライブラリだったらいくらでもいじって大丈夫ですよね。というわけでいい感じにいじってどこが呼び出し元か分かるようにしましょうという話です。

std::stacktraceだと不完全な場合がある

この手の問題が発生した場合、スタックトレースを取るという手段があります。gccでビルドしてるならgdbで走らせたあとbtを叩く、みたいな。
C++標準ではC++23からstd::stacktraceがあります(リンク先cpprefjp)。これでスタックトレースを出力できます。

[[noreturn]] void fail(const char* expr) {
    auto st = std::stacktrace::current();
    // いい感じに出力する
    std::abort();
}
#define MY_ASSERT(expr) (static_cast<bool>(expr) ? void(0) : fail(#expr))

これで一安心、とならないのがC++の難しいところです。例えば-O2オプションを入れると一部の情報が抜け落ちたりします(そもそも-O2入れたままデバッグって何という話もあると思いますが、それはそれとして[1])。

int func0(int v) {
    MY_ASSERT(v != 0);
    return 10 / v;
}

int func1(int v) {
    return 10 * func0(v);
}

int func2(int v) {
    return func1(v) + 3;
}

int main() {
    int c = 0;
    std::cout << func2(c) << std::endl;
}

このコードを、例えばg++ a.cpp -std=c++23 -g -lstdc++exp -O2でビルドして実行すると一部の情報が抜け落ちて出力されたりします。

上のコードで知りたいのは「assertが落ちている直接の理由はfunc2を呼び出している部分」ということです。次の項でstd::source_locationを使う方法を紹介します。

std::source_locationの仕様を理解する

std::source_locationをうまく利用すると呼び出し元が取れます。例えば以下のように改変できます。

#include <iostream>
#include <source_location>

[[noreturn]] void fail(const char* expr, std::source_location from) {
    std::cerr << "assertion \"" << expr << "\" failed" << std::endl;
    std::cerr << "from line:" << from.line() << std::endl;
    std::abort();
}
#define MY_ASSERT(expr) (static_cast<bool>(expr) ? void(0) : fail(#expr, from))

int func0(int v, std::source_location from) {
    MY_ASSERT(v != 0);
    return 10 / v;
}

int func1(int v, std::source_location from) {
    return 10 * func0(v, from);
}

int func2(int v, std::source_location from = std::source_location::current()) {
    return func1(v, from) + 3;
}

int main() {
    int c = 0;
    std::cout << func2(c) << std::endl;
}

これの出力は以下のようになります。

assertion "v != 0" failed
from line:26
Aborted

出力周りは自分の好きなようにカスタマイズするとして、必要そうな情報は取れそうです。

しかしstd::source_location from = std::source_location::current()を一々書くのは邪魔くさいですし、場合によっては[[maybe_unused]]をつけたい場合もあります。あと変数名が被ったりして困ることもあるので被りにくい変数名にしたいです。じゃあマクロを使おうじゃないか。

#include <iostream>
#include <source_location>

[[noreturn]] void fail(const char* expr, std::source_location from) {
    std::cerr << "assertion \"" << expr << "\" failed" << std::endl;
    std::cerr << "from line:" << from.line() << std::endl;
    std::abort();
}
#define MY_ASSERT(expr)                                                        \
    (static_cast<bool>(expr) ? void(0) : fail(#expr, MY_ASSERT_FROM))
#define FROM_LOCATION                                                          \
    std::source_location MY_ASSERT_FROM = std::source_location::current()
int func0(int v, FROM_LOCATION) {
    MY_ASSERT(v != 0);
    return 10 / v;
}

int func1(int v, FROM_LOCATION) {
    return 10 * func0(v, MY_ASSERT_FROM);
}

int func2(int v, FROM_LOCATION) {
    return func1(v, MY_ASSERT_FROM) + 3;
}

int main() {
    int c = 0;
    std::cout << func2(c) << std::endl;
}

なんやかんやで書く量はそれなりに必要ですが、エディタの補助などを利用すればまぁ許容範囲でしょう。これで万事解決ですね。……本当に?

int another_func0(int v, FROM_LOCATION) {
    MY_ASSERT(v != 0);
    return 5 / v;
}

int another_func1(int v, FROM_LOCATION) {
    return another_func0(v) * 5; // MY_ASSERT_FROMを渡していない!
}

int main() {
    int c = 0;
    std::cout << another_func1(c) << std::endl;
}

うっかりMY_ASSERT_FROMを渡し忘れました。するとassertionが失敗したときの情報が正しいものにならなくなります。なんのためのMY_ASSERTなんでしょうか。

このようなうっかりはなくならないものなので何かの対策が欲しいところです。次の項ではラッパーを噛ましてコンストラクタとデストラクタの回数を数えることで入ったタイミングを取得する方法を紹介します。

ラッパーを噛まして入ったタイミングを保持する

当然ですが(よっぽど変なことをしない限り)変数のデストラクタの呼び出し回数がコンストラクタの呼び出し回数を超えることはありません。この回数が同じなのは全ての変数が破棄されているときです。

ということはラッパーを噛ましてコンストラクタの回数とデストラクタの回数をカウントしておくと生きているMY_ASSERT_FROMの個数が分かります。すると

  • 差し引き0でコンストラクタに入った場合、そのタイミングがライブラリ部分に入ったタイミングである
  • デストラクタの終わりで差し引き0になった場合、そのタイミングがライブラリ部分から出たタイミングである

ということになるので、どのタイミングでライブラリに入ったかの情報が取得できます。この方法だとMY_ASSERT_FROMを渡す必要もなくなります。

thread_local std::optional<std::source_location> MY_ASSERT_FROM = std::nullopt;
thread_local int depth = 0;
struct location {
    location(std::source_location loc) {
        if (!MY_ASSERT_FROM) {
            MY_ASSERT_FROM = loc;
        }
        ++depth;
    }
    ~location() {
        if (--depth == 0) {
            MY_ASSERT_FROM.reset();
        }
    }
};
[[noreturn]] void fail(const char* expr); // 省略
#define FROM_LOCATION location MY_ASSERT_DUMMY = std::source_location::current()
#define MY_ASSERT(expr) (static_cast<bool>(expr) ? void(0) : fail(#expr))

int func0(int v, FROM_LOCATION) {
    MY_ASSERT(v != 0);
    return 5 / v;
}

int func1(int v, FROM_LOCATION) {
    return func0(v) * 5;
}

int main() {
    int c = 0;
    std::cout << func1(c) << std::endl;
}

余談1: 変数名の話

記事中のMY_ASSERT_FROMですが、C++の規格上では

  • __を含むか_から始まって次がアルファベット大文字であるトークンは予約されている
  • _から始まるトークンはグローバル名前空間で予約されている

とあるので、例えばnamespace mylib内で定義するのであれば_fromとかでもいいということになります(自分の理解が合っているなら)。

また最後の例でMY_ASSERT_DUMMYとしていますが、C++26からは変数名が_なら暗黙で[[maybe_unused]]属性が付加されシャドウイングも可能なので、環境が許すのならそちらを使うのも手です。

余談2: 可変引数テンプレートで使えない

今回の記事の内容ですが、可変引数テンプレートのみのテンプレート関数で使えないという問題があり、現在進行系で困ってます。よい解決策があったらコメントください。

無理矢理使う方法は何個か思い浮かんではいるのですが、直感的でなかったり余計な呼び出しが増えたりとどれも渋い顔になってしまう感じですね。

候補1: 取得用の関数を挟む方法

std::source_locationを取るための関数を経由して呼び出す方法。operator()が2つになるのが非直感的で不満。

template <typename... Args> auto func_i(Args... args) {
    MY_ASSERT(...);
}

auto func(FROM_LOCATION) {
    return []<typename... Args>(Args&&... args) {
        return func_i(std::forward<Args>(args)...);
    };
};

int main() {
    int a = 0, b = 0;
    func()(a, b);
}
候補2: 遅延評価にする方法

後からstd::source_locationを取るようにする方法。やっぱりoperator()が2つになるのが不満。

template <typename... Args> auto func_i(Args... args) {
    MY_ASSERT(...);
}

template <typename... Args> auto func(Args... args) {
    return [&](location loc = std::source_location::current()) {
        return func_i(std::forward<Args>(args)...);
    };
}

int main() {
    func(1, 2)();
}
候補3: 先頭の引数を専用ラッパーに包む方法

std::source_location取得用のラッパーを挟む方法。locationが破棄されないようにメンバ変数にする必要があります。ちなみにintからget_loc<int>を推論してくれないのでfunc(1)のような書き方はできないです。

template <typename... Args> auto func_i(Args... args) {
    MY_ASSERT(...);
}

template <typename T> struct get_loc {
    T val;
    location loc;
    get_loc(T v, std::source_location l = std::source_location::current())
        : val(v), loc(l) {}
};

template <typename Arg, typename... Args>
auto func(get_loc<Arg> arg, Args... args) {
    return func_i(arg.val, std::forward<Args>(args)...);
}
auto func(FROM_LOCATION) {
    return func_i();
}

int main() {
    func(get_loc(1), 2);
    func();
}

ちなみに第1引数が非テンプレートなら候補3の応用で解決したりします。

template <typename... Args> auto func_i(Args... args) {
    MY_ASSERT(...);
}

template <typename T> struct get_loc {
    T val;
    location loc;
    get_loc(T v, std::source_location l = std::source_location::current())
        : val(v), loc(l) {}
};

template <typename... Args> auto func(get_loc<int> arg, Args... args) {
    return func_i(arg.val, std::forward<Args>(args)...);
}

int main() {
    func(1, 2); // 1がget_loc<int>に暗黙でキャストされるのでOK
}

余談3: 関数オブジェクトを取る関数絡みの話

関数オブジェクトを取る関数だと上手く働かない場合があります。

void func0(int v, FROM_LOCATION) {
    MY_ASSERT(v != 0);
}

void func1(std::function<void(void)> f, FROM_LOCATION) {
    f();
}

void my_func() {
    func0(0); // 根本原因はここ
}

int main() {
    func1(my_func); // ここの行番号が表示される
}

locationのコンストラクタでstd::source_locationの中身をチェックして、ライブラリの外なら入れ替えるという手があります。チェックの手法としては「file_name()がライブラリのファイルを指すかを見る」「function_name()がライブラリの名前空間を含むか見る」などがあります。

struct location {
    std::optional<std::source_location> prev = std::nullopt;
    bool check(std::source_location loc) {
        // チェック
    }
    location(std::source_location loc) {
        if (!MY_ASSERT_FROM) {
            MY_ASSERT_FROM = loc;
        } else if (check(loc)) {
            prev = std::exchange(MY_ASSERT_FROM, loc);
        }
        ++depth;
    }
    ~location() {
        if (prev) {
            MY_ASSERT_FROM = prev;
        }
        if (--depth == 0) {
            MY_ASSERT_FROM.reset();
        }
    }
};

std::source_locationの各メンバ関数はconstexprなので、consteval関数でチェックしておいてその結果をlocationに渡すという方法もあります。

struct location {
    std::optional<std::source_location> prev = std::nullopt;
    location(std::pair<std::source_location, bool> p) {
        const auto& [loc, c] = p;
        if (!MY_ASSERT_FROM) {
            MY_ASSERT_FROM = loc;
        } else if (c) {
            prev = std::exchange(MY_ASSERT_FROM, loc);
        }
        ++depth;
    }
    ~location() {
        if (prev) {
            MY_ASSERT_FROM = prev;
        }
        if (--depth == 0) {
            MY_ASSERT_FROM.reset();
        }
    }
};
consteval std::pair<std::source_location, bool> check(std::source_location loc) {
    // チェック
    return {loc, result};
}
#define FROM_LOCATION location _ = check(std::source_location::current())

余談4: 演算子オーバーロードどうするの

そこはこう、余談2のclass get_locを応用して頑張るしかないです。

template <typename T> struct get_loc {
    T val;
    location loc;
    get_loc(T v, std::source_location l = std::source_location::current())
        : val(v), loc(l) {}
};

struct my_container {
    int operator[](get_loc<int> v) {
        MY_ASSERT(v.val != 0);
        return v.val;
    }
};

struct my_value {
    int a, b;
};

my_value operator+(get_loc<my_value> lhs, get_loc<my_value> rhs) {
    MY_ASSERT(lhs.val.a + lhs.val.b != 0);
    return my_value{0, 0};
}

int main() {
    my_container cont;
    std::cout << cont[0] << std::endl;
    my_value a{}, b{1, 1};
    std::cout << (a + b).a << std::endl;
}

非template引数を持ってるなら全体的にFROM_LOCATIONマクロ使うよりclass get_locを使った方がいい気がしてきた。

終わりに

この記事ではassertの改造をメインに、呼び出し元の情報を取得するための工夫について書いてきました。可変引数テンプレートでうまく使えないのが残念ですが……。

これらのテクニックを応用すると他にも色々できるような気がしますが、記事が膨大になって収集がつかなくなりそうなのでこの記事はここまでとします。

参考文献

脚注
  1. 競プロだと一分一秒を争うのでそんな時間が取れないという事情はあったりします ↩︎

Discussion