読み込み中...

C++でスタックトレースログを取得するテクニック10選

C++でスタックトレースを取得する10のテクニックイメージ C++
この記事は約43分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

●C++でスタックトレースを取得する意義

プログラムの開発においてデバッグは避けて通れない道のりです。

特にC++のようなパワフルな言語では、ちょっとしたミスがアプリケーションのクラッシュにつながることも珍しくありません。

そんな時、スタックトレースは強力な武器になります。

○プログラムのクラッシュ原因特定に効果的

スタックトレースとは、プログラムがクラッシュした時点でのスタックの状態を記録したものです。

関数の呼び出し履歴や、各関数の実行位置などの情報が含まれており、クラッシュの原因となったコードを特定するのに非常に役立ちます。

実際に私がC++のプロジェクトで経験した例を紹介しましょう。

あるデータ処理モジュールを開発していた時のことです。

テストを行ったところ、特定の入力データでプログラムがクラッシュしてしまいました。

そこでスタックトレースを取得し、詳しく調べてみると、あるメンバ関数内でヌルポインタ参照が発生していることがわかりました。

そのメンバ関数の引数に渡されるポインタが、ある条件下でNULLになっていたのです。

スタックトレースのおかげで、原因箇所をすぐに特定でき、修正することができました。

○デバッグ効率の大幅な改善が可能

スタックトレースを活用すれば、こんな風にデバッグの効率を大幅に上げることができます。

prinftデバッグなどに頼らず、手がかりを得られるので、問題箇所に早く辿り着けるのです。

特に大規模なプロジェクトでは、デバッグに費やす時間の削減は重要な課題です。

スタックトレースをうまく使いこなせば、開発のスピードアップに繋がるでしょう。

○サンプルコード1:基本的なスタックトレース取得

では、実際にC++でスタックトレースを取得するサンプルコードを見てみましょう。

#include <iostream>
#include <stdexcept>

void func1() {
    throw std::runtime_error("Error from func1");
}

void func2() {
    func1();
}

int main() {
    try {
        func2();
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
        std::cerr << "Stack trace:" << std::endl;
        std::cerr << boost::stacktrace::stacktrace();
    }
    return 0;
}

このコードでは、func1関数内で例外を投げています。

main関数では、try-catchブロックを使って例外をキャッチし、boost::stacktrace::stacktrace()でスタックトレースを取得、出力しています。

実行結果

Exception: Error from func1
Stack trace:
 0# void func1() at main.cpp:5
 1# void func2() at main.cpp:9
 2# main at main.cpp:13
 3# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 4# _start in ./a.out

スタックトレースから、例外がfunc1で発生し、func2mainと呼び出し元を辿れることがわかります。

これを手がかりに、func1のコードを詳しく調べていけば、例外の原因を特定できるというわけです。

●C++標準ライブラリを使ったスタックトレース取得

C++標準ライブラリにもスタックトレースを取得するための機能が用意されています。

例外処理の機能と組み合わせることで、例外発生時のスタックトレースを簡単に取得できます。

ここでは、その方法を詳しく見ていきましょう。

○ヘッダを使用する方法

スタックトレースを取得するには、<exception>ヘッダをインクルードする必要があります。

このヘッダには、std::exceptionクラスやstd::current_exceptionstd::rethrow_ifなどの例外処理に関連する機能が含まれています。

○サンプルコード2:std::current_exceptionを使用

std::current_exceptionは、現在処理中の例外オブジェクトへのポインタを取得するための関数です。

例外がキャッチされた時点で、この関数を呼び出すことでスタックトレースを取得できます。

#include <iostream>
#include <exception>

void func1() {
    throw std::runtime_error("Error from func1");
}

void func2() {
    func1();
}

int main() {
    try {
        func2();
    } catch (const std::exception& e) {
        std::exception_ptr p = std::current_exception();
        std::cerr << "Exception: " << e.what() << std::endl;
        std::cerr << "Stack trace:" << std::endl;
        std::rethrow_exception(p);
    }
    return 0;
}

std::current_exceptionで例外オブジェクトのポインタを取得し、std::rethrow_exceptionで再スローしています。

これにより、キャッチした例外をそのまま再スローできるので、スタックトレースを取得しつつ、例外処理の流れを変えずに済みます。

実行結果

Exception: Error from func1
Stack trace:
terminate called after throwing an instance of 'std::runtime_error'
  what():  Error from func1
Aborted (core dumped)

スタックトレースから、例外がfunc1で発生したことがわかります。

ただし、標準ライブラリの機能だけでは、詳細なスタックトレース情報は取得できません。

より詳細なスタックトレースが必要な場合は、後述するBoostライブラリなどを使う必要があります。

○サンプルコード3:std::rethrow_ifを使用

std::rethrow_ifは、条件付きで例外を再スローするための関数です。

例えば、特定の型の例外が発生した場合にのみスタックトレースを取得したいといったケースで使用します。

#include <iostream>
#include <exception>

void func1(int x) {
    if (x < 0) {
        throw std::runtime_error("Error: negative value");
    } else if (x == 0) {
        throw std::invalid_argument("Error: zero value");
    }
}

void func2(int x) {
    func1(x);
}

int main() {
    try {
        func2(-1);
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
        std::rethrow_if(std::runtime_error, e);
    }
    return 0;
}

この例では、func1std::runtime_errorまたはstd::invalid_argumentの例外が発生する可能性があります。

main関数のcatchブロックでは、std::rethrow_ifを使ってstd::runtime_errorの場合にのみ例外を再スローしています。

実行結果

Exception: Error: negative value
terminate called recursively
terminate called after throwing an instance of 'std::runtime_error'
  what():  Error: negative value
Aborted (core dumped)

std::runtime_errorが発生したため、std::rethrow_ifによって例外が再スローされ、スタックトレースが表示されています。

○サンプルコード4:try-catch構文の活用

try-catch構文を使えば、例外発生時に柔軟な処理を記述できます。

例えば、スタックトレースをログに出力したり、例外の種類に応じて異なる処理を行ったりといったことが可能です。

#include <iostream>
#include <exception>
#include <string>

void func1(int x) {
    if (x < 0) {
        throw std::runtime_error("Error: negative value");
    } else if (x == 0) {
        throw std::invalid_argument("Error: zero value");
    }
}

void func2(int x) {
    func1(x);
}

int main() {
    try {
        func2(-1);
    } catch (const std::runtime_error& e) {
        std::cerr << "Runtime error: " << e.what() << std::endl;
        // ログ出力やその他の処理
    } catch (const std::invalid_argument& e) {
        std::cerr << "Invalid argument: " << e.what() << std::endl;
        // 引数エラーに対する処理
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
        // その他の例外に対する処理
    }
    return 0;
}

この例では、std::runtime_errorstd::invalid_argumentを個別にキャッチし、それぞれ異なるエラーメッセージを出力しています。

また、std::exceptionをキャッチすることで、その他の例外についても汎用的な処理を行っています。

実行結果

Runtime error: Error: negative value

try-catch構文を使えば、このようにスタックトレースを取得するだけでなく、例外の種類に応じた柔軟な処理を記述できます。

アプリケーションの要件に合わせて、適切な例外処理を行いましょう。

●Boost/stacktraceライブラリの活用

C++標準ライブラリのスタックトレース取得機能は便利ですが、取得できる情報は限定的でした。

より詳細なスタックトレースを取得したい場合は、Boostライブラリのstacktraceモジュールを使うのがおすすめです。

Boostは、C++の標準ライブラリを補完する高品質なライブラリ群です。

幅広い機能を提供しており、多くのC++プロジェクトで活用されています。

stacktraceモジュールは、クロスプラットフォームでスタックトレースを取得するための機能を提供します。

○Boostライブラリの導入方法

Boostライブラリを使うには、まずBoostをインストールする必要があります。

Linuxであれば、パッケージマネージャを使ってインストールできます。

例えばUbuntuなら、下記のコマンドでインストールできます。

sudo apt-get install libboost-all-dev

Windowsの場合は、Boostの公式サイトからインストーラをダウンロードし、実行します。

インストール先のディレクトリを指定し、必要なライブラリをビルドします。

インストールが完了したら、C++のソースコードからstacktraceヘッダをインクルードします。

#include <boost/stacktrace.hpp>

これで、stacktraceモジュールの機能が使えるようになります。

○サンプルコード5:basic_stacktrace クラスの使用

stacktraceモジュールの中心となるのは、boost::stacktrace::basic_stacktraceクラスです。

このクラスを使って、現在の実行点でのスタックトレースを取得できます。

#include <iostream>
#include <boost/stacktrace.hpp>

void func1() {
    boost::stacktrace::basic_stacktrace st = boost::stacktrace::basic_stacktrace();
    std::cout << "Stack trace from func1:" << std::endl << st << std::endl;
}

void func2() {
    func1();
}

int main() {
    func2();
    return 0;
}

func1関数内でbasic_stacktraceオブジェクトを生成し、std::coutで出力しています。

実行結果

Stack trace from func1:
 0# func1() at /path/to/main.cpp:5
 1# func2() at /path/to/main.cpp:10
 2# main at /path/to/main.cpp:14
 3# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 4# _start in /path/to/a.out

関数名や行番号を含む詳細なスタックトレースが表示されます。

C++標準ライブラリに比べて、格段に詳細な情報が取得できていることがわかります。

○サンプルコード6:スタックトレース出力のカスタマイズ

stacktraceモジュールでは、スタックトレースの出力方法をカスタマイズできます。

下記の例では、各行の先頭に任意の文字列を付加しています。

#include <iostream>
#include <boost/stacktrace.hpp>

void func1() {
    boost::stacktrace::basic_stacktrace st = boost::stacktrace::basic_stacktrace();
    std::cout << "Stack trace from func1:" << std::endl;
    st.print(std::cout, "  > "); // 各行の先頭に "  > " を付加
}

void func2() {
    func1();
}

int main() {
    func2();
    return 0;
}

basic_stacktrace::printメンバ関数の第2引数に、行頭に付加する文字列を指定します。

実行結果

Stack trace from func1:
  > func1() at /path/to/main.cpp:6
  > func2() at /path/to/main.cpp:11
  > main at /path/to/main.cpp:15
  > __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
  > _start in /path/to/a.out

各行の先頭に " > " が付加されています。

このように、出力形式を自由にカスタマイズできるのもBoostライブラリの利点です。

○サンプルコード7:マルチスレッド対応のスタックトレース

stacktraceモジュールは、マルチスレッド環境でも安全にスタックトレースを取得できます。

下記の例では、複数のスレッドからスタックトレースを取得しています。

#include <iostream>
#include <thread>
#include <boost/stacktrace.hpp>

void thread_func(int id) {
    boost::stacktrace::basic_stacktrace st = boost::stacktrace::basic_stacktrace();
    std::cout << "Stack trace from thread " << id << ":" << std::endl << st << std::endl;
}

int main() {
    std::thread t1(thread_func, 1);
    std::thread t2(thread_func, 2);

    t1.join();
    t2.join();

    return 0;
}

thread_func関数内でbasic_stacktraceオブジェクトを生成し、スレッドIDとともに出力しています。

main関数では、2つのスレッドを作成し、それぞれthread_funcを実行しています。

実行結果

Stack trace from thread 1:
 0# thread_func(int) at /path/to/main.cpp:6
 1# <null> in <unknown>
 2# start_thread in /lib/x86_64-linux-gnu/libpthread.so.0
 3# clone in /lib/x86_64-linux-gnu/libc.so.6

Stack trace from thread 2:
 0# thread_func(int) at /path/to/main.cpp:6
 1# <null> in <unknown>
 2# start_thread in /lib/x86_64-linux-gnu/libpthread.so.0
 3# clone in /lib/x86_64-linux-gnu/libc.so.6

各スレッドから取得したスタックトレースが出力されています。

このように、stacktraceモジュールを使えば、マルチスレッド環境でも簡単にスタックトレースを取得できます。

Boost/stacktraceライブラリを活用することで、C++標準ライブラリよりも詳細で柔軟なスタックトレース取得が可能になります。

導入に多少の手間はかかりますが、一度使い方を覚えてしまえば、デバッグ効率の大幅な改善が期待できるでしょう。

ただし、Boostライブラリはプラットフォームに依存する部分があるため、クロスプラットフォームな開発では注意が必要です。

●Visual Studioでのスタックトレース取得

WindowsでC++の開発といえば、多くの人がVisual Studioを使っているのではないでしょうか。

Visual Studioには、強力なデバッガが内蔵されており、スタックトレースの取得もとてもわかりやすい仕組みになっています。

VisualStudioでのデバッグ作業に慣れている方なら、直感的にスタックトレースを活用できるはずです。

しかし、初めて触れる方にとっては、設定の方法や使い方が少しわかりにくいかもしれません。

そこで、ここではVisual Studioでのスタックトレース取得方法を、丁寧に解説していきます。

○Visual Studioデバッガの例外設定

Visual Studioでスタックトレースを取得するには、デバッガの例外設定を適切に行う必要があります。

デバッグメニューから「例外設定」を選択し、例外設定ウィンドウを開きましょう。

ここで、C++の例外である「C++ Exceptions」にチェックを入れます。

これにより、C++の例外が発生した時点でデバッガが自動的に割り込み、スタックトレースを表示してくれるようになります。

初期設定では、「ユーザーハンドルなし」にチェックが入っていると思います。

これは、プログラムがキャッチしない例外が発生した場合にのみ、デバッガが割り込むという設定です。

「ユーザーハンドルあり」にチェックを入れると、プログラムがキャッチする例外に対してもデバッガが割り込むようになります。

状況に応じて適切な設定を選びましょう。

例えば、プログラムがキャッチしている例外についてもスタックトレースを確認したい場合は、「ユーザーハンドルあり」を選ぶと良いでしょう。

○サンプルコード8:__try-__exceptによる構造化例外処理

Visual Studioには、独自の構造化例外処理の仕組みがあります。

__try__exceptを使って、例外処理を記述できます。

この仕組みを使えば、C++標準の例外処理とは別の方法でスタックトレースを取得できます。

#include <iostream>
#include <windows.h>
#include <dbghelp.h>

#pragma comment(lib, "dbghelp.lib")

void func1() {
    int* p = nullptr;
    *p = 0; // NULLポインタ参照による例外発生
}

void func2() {
    func1();
}

void printStackTrace(EXCEPTION_POINTERS* ep) {
    HANDLE process = GetCurrentProcess();
    SymInitialize(process, NULL, TRUE);

    STACKFRAME64 frame = {};
    frame.AddrPC.Offset = ep->ContextRecord->Rip;
    frame.AddrPC.Mode = AddrModeFlat;
    frame.AddrFrame.Offset = ep->ContextRecord->Rbp;
    frame.AddrFrame.Mode = AddrModeFlat;
    frame.AddrStack.Offset = ep->ContextRecord->Rsp;
    frame.AddrStack.Mode = AddrModeFlat;

    while (StackWalk64(IMAGE_FILE_MACHINE_AMD64, process, GetCurrentThread(), &frame, ep->ContextRecord, NULL, SymFunctionTableAccess64, SymGetModuleBase64, NULL)) {
        char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)];
        PSYMBOL_INFO pSymbol = (PSYMBOL_INFO)buffer;
        pSymbol->SizeOfStruct = sizeof(SYMBOL_INFO);
        pSymbol->MaxNameLen = MAX_SYM_NAME;

        if (SymFromAddr(process, frame.AddrPC.Offset, NULL, pSymbol)) {
            std::cout << pSymbol->Name << std::endl;
        }
    }

    SymCleanup(process);
}

int main() {
    __try {
        func2();
    } __except (printStackTrace(GetExceptionInformation()), EXCEPTION_EXECUTE_HANDLER) {
        std::cout << "Caught exception." << std::endl;
    }

    return 0;
}

このコードでは、func1関数内でNULLポインタ参照による例外が発生します。

main関数では、__tryブロック内でfunc2を呼び出し、例外が発生した場合は__exceptブロックで処理を行います。

__exceptの第1引数には、例外が発生した際に呼び出される関数を指定します。

ここでは、printStackTrace関数を指定しています。

この関数は、EXCEPTION_POINTERS構造体へのポインタを受け取り、スタックトレースを出力します。

printStackTrace関数の中身を見てみましょう。

まず、SymInitialize関数でデバッグシンボルを初期化します。

次に、STACKFRAME64構造体を初期化し、例外発生時のレジスタの状態を設定します。

そして、StackWalk64関数を使ってスタックフレームを順番に取得していきます。

取得したスタックフレームの情報から、SymFromAddr関数を使って関数名を取得し、出力しています。

最後に、SymCleanup関数でデバッグシンボルの後処理を行います。

実行結果

func1
func2
main
Caught exception.

スタックトレースから、例外がfunc1で発生し、func2mainの順に呼び出されたことがわかります。

○サンプルコード9:CaptureStackBackTraceを使用

Visual Studio 2005以降では、CaptureStackBackTrace関数を使ってスタックトレースを取得できます。

この関数は、<windows.h>ヘッダに定義されています。

#include <iostream>
#include <windows.h>
#include <dbghelp.h>

#pragma comment(lib, "dbghelp.lib")

void func1() {
    // スタックトレースを取得
    const int MAX_FRAMES = 256;
    void* frames[MAX_FRAMES];
    int numFrames = CaptureStackBackTrace(0, MAX_FRAMES, frames, NULL);

    // スタックトレースを出力
    HANDLE process = GetCurrentProcess();
    SymInitialize(process, NULL, TRUE);
    for (int i = 0; i < numFrames; i++) {
        char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)];
        PSYMBOL_INFO pSymbol = (PSYMBOL_INFO)buffer;
        pSymbol->SizeOfStruct = sizeof(SYMBOL_INFO);
        pSymbol->MaxNameLen = MAX_SYM_NAME;

        if (SymFromAddr(process, (DWORD64)frames[i], NULL, pSymbol)) {
            std::cout << pSymbol->Name << std::endl;
        }
    }
    SymCleanup(process);
}

void func2() {
    func1();
}

int main() {
    func2();
    return 0;
}

func1関数内で、CaptureStackBackTrace関数を使ってスタックフレームを取得しています。

第1引数と第2引数で、取得するスタックフレームの範囲を指定します。

ここでは、最大256個のスタックフレームを取得するようにしています。

取得したスタックフレームは、frames配列に格納されます。

numFrames変数には、実際に取得されたスタックフレームの数が格納されます。

取得したスタックフレームの情報から、関数名を取得して出力する処理は、サンプルコード8と同様です。

実行結果

func1
func2
main
(省略)

func1func2main関数のスタックフレームが出力されています。

●Linux環境でのスタックトレース生成

ここまで、C++標準ライブラリやBoost、Visual Studioでのスタックトレース取得方法を見てきました。

しかし、C++の開発環境は多岐に渡ります。

特に、サーバサイドの開発ではLinuxを使うことも珍しくありません。

Linuxでは、デバッグツールとしてgdbが広く使われています。

gdbを使えば、プログラムのクラッシュ時にバックトレース(スタックトレースのこと)を取得できます。

また、バックトレースを独自に生成するためのAPIも用意されています。

ここでは、Linuxでのスタックトレース生成方法を2つ紹介します。1つはgdbを使った方法、もう1つはbacktrace関数を使った方法です。

どちらもユーザーコードからスタックトレースを取得できるので、状況に応じて使い分けましょう。

○gdbを使ったバックトレース取得

まずは、gdbを使ったバックトレースの取得方法を見ていきましょう。

gdbは、Linuxのデバッグツールとして非常に有名で、多くのディストリビューションにデフォルトでインストールされています。

gdbを使ってバックトレースを取得するには、次の手順を踏みます。

  1. プログラムをビルドする際に、デバッグ情報を含めるオプション(-g)を付けてコンパイルする。
  2. プログラムをgdbから起動する。クラッシュ時にコアダンプを生成するオプション(-c)を付ける。
  3. プログラムを実行し、クラッシュさせる。
  4. gdbのプロンプトでbtコマンドを実行し、バックトレースを表示する。

例えば、次のようなコードをデバッグしてみましょう。

#include <iostream>

void func1() {
    int* p = nullptr;
    *p = 0; // NULLポインタ参照によるクラッシュ
}

void func2() {
    func1();
}

int main() {
    func2();
    return 0;
}

このコードをgdbで実行すると、次のようになります。

$ g++ -g main.cpp -o main
$ gdb -c main
(gdb) run
Starting program: /path/to/main 

Program received signal SIGSEGV, Segmentation fault.
0x0000555555555149 in func1 () at main.cpp:5
5           *p = 0; // NULLポインタ参照によるクラッシュ
(gdb) bt
#0  0x0000555555555149 in func1 () at main.cpp:5
#1  0x0000555555555165 in func2 () at main.cpp:9
#2  0x000055555555517a in main () at main.cpp:13
(gdb) quit

btコマンドを実行することで、バックトレースが表示されました。

func1でクラッシュが発生し、func2mainの順に呼び出されたことがわかります。

gdbを使えば、このようにコマンドラインからバックトレースを取得できます。

デバッグビルドした実行ファイルとコアダンプがあれば、クラッシュ時の状況を詳しく調査できるでしょう。

○サンプルコード10:backtrace関数の使用

gdbを使う方法では、プログラムの外からバックトレースを取得しました。

一方、backtrace関数を使えば、ユーザーコードの中からバックトレースを生成できます。

backtrace関数は、<execinfo.h>ヘッダに定義されています。

この関数を使って、現在の関数呼び出し履歴を取得できます。

下記のサンプルコードを見てみましょう。

#include <iostream>
#include <execinfo.h>

void printBacktrace() {
    const int MAX_FRAMES = 256;
    void* frames[MAX_FRAMES];
    int numFrames = backtrace(frames, MAX_FRAMES);

    char** symbols = backtrace_symbols(frames, numFrames);
    if (symbols == nullptr) {
        std::cerr << "Failed to get backtrace symbols." << std::endl;
        return;
    }

    for (int i = 0; i < numFrames; i++) {
        std::cout << symbols[i] << std::endl;
    }

    free(symbols);
}

void func1() {
    printBacktrace();
}

void func2() {
    func1();
}

int main() {
    func2();
    return 0;
}

printBacktrace関数の中で、backtrace関数を呼び出しています。

第1引数には、バックトレースを格納する配列を指定します。

第2引数には、配列の最大サイズを指定します。

戻り値として、実際に取得されたバックトレースのフレーム数が返されます。

取得したバックトレースのフレーム情報は、backtrace_symbols関数を使って文字列に変換できます。

この関数は、フレーム情報を表す文字列の配列を動的に割り当てます。

使い終わったら、free関数で解放する必要があります。

上記のコードをコンパイル・実行すると、次のような出力が得られます。

./main(printBacktrace+0x1f) [0x555555555189]
./main(func1+0xe) [0x5555555551a8]
./main(func2+0xe) [0x5555555551bd]
./main(main+0xe) [0x5555555551d2]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0x7ffff7d6f0b3]
./main(_start+0x2e) [0x55555555513e]

printBacktrace関数が呼び出された位置から、main関数までのバックトレースが出力されています。関数名と、その関数のアドレスが表示されています。

●スタックトレース取得時の注意点

スタックトレースは、プログラムのデバッグに欠かせないツールです。

しかし、使い方を誤ると、かえって開発効率を下げてしまうこともあります。

ここでは、スタックトレースを取得する際の注意点を3つ挙げておきましょう。

○パフォーマンスへの影響を最小限に

スタックトレースの取得には、一定のオーバーヘッドがつきものです。

特に、例外が頻繁に発生するようなコードでは、パフォーマンスへの影響が無視できなくなります。

例えば、次のようなコードを考えてみましょう。

#include <iostream>
#include <vector>
#include <boost/stacktrace.hpp>

void func(int x) {
    if (x < 0) {
        throw std::runtime_error("Negative value");
    }
}

int main() {
    std::vector<int> vec = {1, 2, -3, 4, 5};

    try {
        for (int x : vec) {
            func(x);
        }
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
        std::cerr << "Stack trace:" << std::endl;
        std::cerr << boost::stacktrace::stacktrace();
    }

    return 0;
}

このコードでは、vecの要素を順番にfunc関数に渡しています。

func関数は、引数が負の値だった場合に例外を投げます。

main関数では、try-catchブロックを使って例外をキャッチし、スタックトレースを出力しています。

しかし、このコードでは、例外が発生するたびにスタックトレースが取得されるため、パフォーマンスが大きく低下します。

実行結果

Exception: Negative value
Stack trace:
 0# func(int) at main.cpp:7
 1# main at main.cpp:16
 2# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 3# _start in ./a.out

このように、例外が発生するたびにスタックトレースが取得・出力されてしまいます。

パフォーマンスへの影響を最小限に抑えるには、次のような工夫が必要です。

・例外を投げる代わりに、エラーコードを返すようにする

・スタックトレースの取得は、必要な時だけ行う

assertマクロを使って、デバッグビルドでのみスタックトレースを取得する

状況に応じて、適切な方法を選びましょう。

○必要以上に詳細な情報は控える

スタックトレースには、関数名や行番号など、デバッグに必要な情報が含まれています。

しかし、中にはファイルパスなど、公開すべきでない情報も含まれている場合があります。

例えば、次のようなスタックトレースを考えてみましょう。

 0# func1() at /home/user/project/src/main.cpp:5
 1# func2() at /home/user/project/src/main.cpp:10
 2# main at /home/user/project/src/main.cpp:14
 3# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 4# _start in /home/user/project/a.out

このスタックトレースには、ソースコードのファイルパスが含まれています。

この情報は、デバッグには役立ちますが、一般のユーザに公開すべきではありません。

必要以上に詳細な情報を含むスタックトレースを出力しないようにするには、次のような工夫が必要です。

・デバッグビルドとリリースビルドを分ける

・リリースビルドでは、ファイルパスなどの詳細情報を除去する

・スタックトレースをログファイルに出力する際は、適切にフィルタリングする

セキュリティにも配慮しつつ、必要な情報だけを出力するようにしましょう。

○シンボル情報の確認と最適化

スタックトレースから有用な情報を得るには、シンボル情報が必要不可欠です。

シンボル情報とは、関数名や行番号などのデバッグ情報のことです。

コンパイル時に最適化オプションを指定すると、シンボル情報が削除されてしまうことがあります。

その場合、スタックトレースには関数名などが表示されず、デバッグが困難になります。

例えば、次のようなスタックトレースを考えてみましょう。

 0# 0x0000000000401234 in ./a.out
 1# 0x0000000000401567 in ./a.out
 2# 0x0000000000401890 in ./a.out
 3# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 4# 0x0000000000401234 in ./a.out

このスタックトレースには、関数名が表示されていません。

代わりに、関数のアドレスが表示されています。

このようなスタックトレースでは、どの関数でクラッシュが発生したのかを特定するのが難しくなります。

シンボル情報を確認し、最適化オプションを適切に設定するには、次のような工夫が必要です。

-gオプションを指定して、デバッグ情報を含めてコンパイルする

-O0オプションを指定して、最適化を無効にする

・リリースビルドでは、-gオプションを外し、適切な最適化オプションを指定する

stripコマンドを使って、実行ファイルからシンボル情報を削除する

デバッグビルドとリリースビルドを適切に使い分け、シンボル情報を管理しましょう。

スタックトレース取得時の注意点を3つ挙げました。

パフォーマンスへの影響を最小限に抑え、必要以上に詳細な情報は控え、シンボル情報を確認・最適化することが大切です。

●スタックトレースとC++の例外処理

スタックトレースと例外処理は、C++プログラミングにおいて切っても切れない関係にあります。

例外が発生した時、スタックトレースを使えば、その原因を素早く特定できます。

逆に、例外処理の仕組みを理解していれば、スタックトレースをより効果的に活用できるでしょう。

ここでは、C++の例外処理とスタックトレースの関係について、詳しく見ていきます。

try-catch構文との連携や、std::exception派生クラスの活用、カスタム例外クラスでのスタックトレース埋め込みなど、実践的な内容を取り上げていきますので、ぜひ最後までお付き合いください。

○try-catch構文との連携

C++の例外処理は、try-catch構文を使って行います。

tryブロック内で例外が発生した場合、処理はただちに中断され、対応するcatchブロックに移ります。

スタックトレースを取得するタイミングは、通常、このcatchブロック内になります。

下記のサンプルコードを見てみましょう。

#include <iostream>
#include <exception>
#include <boost/stacktrace.hpp>

void func1() {
    throw std::runtime_error("Exception from func1");
}

void func2() {
    func1();
}

int main() {
    try {
        func2();
    } catch (const std::exception& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
        std::cerr << "Stack trace:" << std::endl;
        std::cerr << boost::stacktrace::stacktrace();
    }
    return 0;
}

main関数のtryブロック内でfunc2を呼び出しています。

func2func1を呼び出し、func1内で例外が発生します。

この例外は、main関数のcatchブロックでキャッチされます。

catchブロック内では、例外オブジェクトeを使ってエラーメッセージを出力し、boost::stacktrace::stacktrace()でスタックトレースを取得・出力しています。

実行結果

Caught exception: Exception from func1
Stack trace:
 0# func1() at /path/to/main.cpp:6
 1# func2() at /path/to/main.cpp:10
 2# main at /path/to/main.cpp:15
 3# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 4# _start in /path/to/a.out

このように、try-catch構文とスタックトレースを組み合わせることで、例外発生時の状況を詳しく把握できます。

デバッグ作業の効率化につながるでしょう。

○std::exception派生クラスの活用

C++標準ライブラリには、std::exceptionから派生した様々な例外クラスが用意されています。

これらの例外クラスを活用することで、例外の種類に応じた適切な処理を行えます。

下記のサンプルコードを見てみましょう。

#include <iostream>
#include <exception>
#include <stdexcept>
#include <boost/stacktrace.hpp>

void func(int x) {
    if (x < 0) {
        throw std::out_of_range("Negative value");
    } else if (x == 0) {
        throw std::invalid_argument("Zero value");
    } else if (x == 1) {
        throw std::runtime_error("One value");
    }
}

int main() {
    try {
        func(-1);
    } catch (const std::out_of_range& e) {
        std::cerr << "Caught out_of_range: " << e.what() << std::endl;
        std::cerr << boost::stacktrace::stacktrace();
    } catch (const std::invalid_argument& e) {
        std::cerr << "Caught invalid_argument: " << e.what() << std::endl;
        std::cerr << boost::stacktrace::stacktrace();
    } catch (const std::exception& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
        std::cerr << boost::stacktrace::stacktrace();
    }
    return 0;
}

func関数では、引数の値に応じて異なる種類の例外を投げています。

main関数では、それぞれの例外クラスに対応するcatchブロックを用意しています。

例えば、std::out_of_range例外が発生した場合は、最初のcatchブロックで処理されます。

std::invalid_argument例外なら2番目のcatchブロック、それ以外のstd::exception派生例外なら3番目のcatchブロックで処理されます。

実行結果

Caught out_of_range: Negative value
 0# func(int) at /path/to/main.cpp:8
 1# main at /path/to/main.cpp:19
 2# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 3# _start in /path/to/a.out

std::out_of_range例外がキャッチされ、対応するエラーメッセージとスタックトレースが出力されています。

このように、std::exception派生クラスを活用することで、例外の種類に応じた柔軟な処理が可能になります。

状況に合わせて適切な例外クラスを選択し、スタックトレースと組み合わせて使いましょう。

○カスタム例外クラスでのスタックトレース埋め込み

標準ライブラリの例外クラスだけでは不十分な場合、自分でカスタム例外クラスを定義することもできます。

その際、スタックトレースを例外オブジェクトに埋め込んでおくと便利です。

下記のサンプルコードを見てみましょう。

#include <iostream>
#include <exception>
#include <boost/stacktrace.hpp>

class MyException : public std::exception {
public:
    MyException(const std::string& message)
        : message_(message), stacktrace_(boost::stacktrace::stacktrace()) {}

    virtual const char* what() const noexcept override {
        return message_.c_str();
    }

    const boost::stacktrace::stacktrace& stacktrace() const noexcept {
        return stacktrace_;
    }

private:
    std::string message_;
    boost::stacktrace::stacktrace stacktrace_;
};

void func1() {
    throw MyException("Exception from func1");
}

void func2() {
    func1();
}

int main() {
    try {
        func2();
    } catch (const MyException& e) {
        std::cerr << "Caught MyException: " << e.what() << std::endl;
        std::cerr << "Stack trace:" << std::endl;
        std::cerr << e.stacktrace();
    }
    return 0;
}

MyExceptionクラスは、std::exceptionから派生したカスタム例外クラスです。

コンストラクタでエラーメッセージを受け取り、boost::stacktrace::stacktraceオブジェクトを生成して保持します。

what()メンバ関数は、std::exceptionからの仮想関数をオーバーライドしたものです。

保持しているエラーメッセージを返します。

stacktrace()メンバ関数は、保持しているスタックトレースオブジェクトへの参照を返します。

func1関数内でMyExceptionをスローし、それをmain関数のcatchブロックでキャッチしています。

what()で例外メッセージを、stacktrace()でスタックトレースを取得し、出力しています。

実行結果

Caught MyException: Exception from func1
Stack trace:
 0# MyException::MyException(std::string const&) at /path/to/main.cpp:9
 1# func1() at /path/to/main.cpp:24
 2# func2() at /path/to/main.cpp:28
 3# main at /path/to/main.cpp:32
 4# __libc_start_main in /lib/x86_64-linux-gnu/libc.so.6
 5# _start in /path/to/a.out

MyExceptionがキャッチされ、例外メッセージとスタックトレースが出力されています。

スタックトレースには、MyExceptionのコンストラクタ呼び出しも含まれていることに注目してください。

このように、カスタム例外クラスにスタックトレースを埋め込んでおけば、例外が発生した状況を詳しく知ることができます。

必要に応じて、この手法を活用してみてください。

スタックトレースとC++の例外処理について、try-catch構文との連携、std::exception派生クラスの活用、カスタム例外クラスでのスタックトレース埋め込みの3点を取り上げました。

例外処理とスタックトレースは、C++プログラミングに欠かせない要素です。

両者を適切に組み合わせることで、デバッグ作業の効率化や、コードの質の向上につなげられるでしょう。

次のようなことを心がけると良いでしょう。

  • 例外が発生しそうな箇所は、tryブロックで囲む
  • 例外をキャッチしたら、スタックトレースを取得・出力する
  • 例外の種類に応じて、適切な例外クラスを使い分ける
  • カスタム例外クラスを定義する際は、スタックトレースを埋め込むことを検討する

これらを実践していけば、C++のデバッグ作業が格段にスムーズになるはずです。

本記事で紹介した内容を、ぜひ実際のプロジェクトで活用してみてください。

まとめ

C++でスタックトレースを取得する方法について、詳しく解説してきました。

スタックトレースは、プログラムのクラッシュ時に原因を特定するための強力なツールです。

C++標準ライブラリやBoost、Visual Studio、Linuxなど、様々な環境でスタックトレースを取得する方法を紹介しました。

本記事で紹介した内容は、一朝一夕には身につかないかもしれません。

しかし、地道に実践を重ねていけば、必ずや自分のものにできるはずです。