An Embedded Engineer’s Blog

とある組み込みエンジニアの備忘録的なブログです。

C++で例外発生箇所(ファイルパス/ファイル行番号/関数名)の特定

まえがき

今回は、C++で例外発生箇所を特定する方法についてです。

C++C#といったオブジェクト指向言語では、エラー処理は例外(Exception)という機構を使うことが一般的です。
C++では、例外をthrowするときにエラー情報を文字列として渡すことで、例外をcatchしたときにその文字列を参照することが出来ます。

try
{
    /* エラーメッセージを指定して例外をthrow */
    throw std::runtime_error("Error Message");
}
/* 例外をcatch */
catch(const std::exception& ex)
{
    /* エラーメッセージを標準エラー出力に表示 */
    std::cerr << ex.what() << std::endl;
}


しかし、文字列の情報だけだと、どの関数でどういう状況で例外が発生したかを特定するのが困難なことがよくあります。
C#などの場合は、スタックトレース(関数の呼出履歴)などの情報も参照することが出来ますが、C++デフォルトでは見ることが出来ません。

そこで、今回は標準で定義されている例外を拡張して、例外発生箇所やスタックトレースを取得する方法を実装してみたいと思います。

前提条件

今回実装したソースは以下の環境でビルド、動作確認しています。

OS Ver Compiler Remarks
Windows 10 Visual Studio 2019
macOS 10.15.6 Clang 11.0
Ubuntu 18.04 GCC 7.5.0


例外発生箇所の特定

まずは、例外発生箇所を特定できるような例外クラスを定義します。

ExceptionBaseクラス

ExceptionBaseクラスは、例外発生箇所を特定する情報を持った基底クラスです。 このクラスを継承した例外クラスを定義して使用します。

【ExceptionBase.h】

#pragma once

#include <exception>
#include <string>

namespace exception
{
    /* エラー情報 */
    struct ErrorInfo
    {
        /* ファイルパス */
        std::string file_path;

        /* 関数名 */
        std::string function_name;

        /* ファイル行番号 */
        int line_number;

        /* コンストラクタ */
        ErrorInfo(const std::string& file, const std::string& func, const int line)
            : file_path(file)
            , function_name(func)
            , line_number(line)
        {
            /* Nothing to do */
        }
    };

    /* Exception Baseクラス宣言 */
    class ExceptionBase : public std::exception
    {
    public:
        /* コンストラクタ */
        ExceptionBase(const std::string& message);

        /* コンストラクタ */
        ExceptionBase(const std::string& message, const std::string& file, const std::string& func, const int line);

        /* デストラクタ */
        virtual ~ExceptionBase();

        /* ファイルパスを取得 */
        const std::string& GetFilePath();

        /* 関数名を取得 */
        const std::string& GetFunctionName();

        /* ファイル行番号を取得 */
        int GetLineNumber();

        /* エラー情報を取得 */
        const ErrorInfo& GetErrorInfo();

        /* エラー要因を取得 */
        virtual char const* what() const noexcept override;

    protected:
        /* エラーメッセージ */
        std::string m_Message;
        /* ファイルパス */
        std::string m_FilePath;
        /* 関数名 */
        std::string m_FunctionName;
        /* ファイル行番号 */
        int m_LineNumber;
        /* エラー情報 */
        ErrorInfo m_ErrorInfo;
        /* エラー情報有無 */
        bool m_IsErrorInfoExists;
    };
}


【ExceptionBase.cpp】

#include "ExceptionBase.h"

#include <sstream>

namespace exception
{
    /* コンストラクタ */
    ExceptionBase::ExceptionBase(const std::string& message)
        : m_Message(message)
        , m_FilePath("")
        , m_FunctionName("")
        , m_LineNumber(0)
        , m_ErrorInfo("", "", 0)
        , m_IsErrorInfoExists(false)
    {
        /* Nothing to do */
    }

    /* コンストラクタ */
    ExceptionBase::ExceptionBase(const std::string& message, const std::string& file, const std::string& func, const int line)
        : m_Message(message)
        , m_FilePath(file)
        , m_FunctionName(func)
        , m_LineNumber(line)
        , m_ErrorInfo(file, func, line)
        , m_IsErrorInfoExists(true)
    {
        /* Nothing to do */
    }

    /* デストラクタ */
    ExceptionBase::~ExceptionBase()
    {
        /* Nothing to do */
    }

    /* ファイルパスを取得 */
    const std::string& ExceptionBase::GetFilePath()
    {
        return this->m_FilePath;
    }

    /* 関数名を取得 */
    const std::string& ExceptionBase::GetFunctionName()
    {
        return this->m_FunctionName;
    }

    /* ファイル行番号を取得 */
    int ExceptionBase::GetLineNumber()
    {
        return this->m_LineNumber;
    }

    /* エラー情報を取得 */
    const ErrorInfo& ExceptionBase::GetErrorInfo()
    {
        return this->m_ErrorInfo;
    }

    /* エラー要因を取得 */
    char const* ExceptionBase::what() const noexcept
    {
        /* エラー情報が存在する場合 */
        if (this->m_IsErrorInfoExists == true)
        {
            std::stringstream ss;

            /* エラーメッセージに、エラー発生箇所(関数名、ファイルパス、ファイル行番号)を付加 */
            ss << this->m_Message << " @ " << this->m_FunctionName << "[" << this->m_FilePath << ": L." << this->m_LineNumber << "]";

            std::string message = ss.str();

            return message.c_str();
        }
        /* エラー情報が存在しない場合 */
        else
        {
            /* エラーメッセージのみ出力 */
            return this->m_Message.c_str();
        }
    }
}

ExceptionBaseクラスでは、コンストラクタの引数としてエラーメッセージ(message)以外に、ファイルパス(file)、関数名(func)、ファイル行番号(line)を渡せるようにします。

また、各種エラー情報を取得するための公開関数(GetFilePath / GetFunctionName / GetLineNumber / GetErrorInfo)を定義しています。

更に、エラーメッセージを取得するwhat関数をオーバーライドし、エラー情報(ファイルパス、行番号、関数名)が渡されている場合は、エラーメッセージにエラー発生箇所を付加して返すようにします。


ExceptionBase継承サンプル

ExceptionBaseクラスを継承した例外クラスのサンプルとして、以下の2種類を実装します。

  • AppException
  • SocketException

基本的な方針としては、以下の方法で例外クラスを定義します。

  • ExceptionBaseクラスを継承する
  • 必要に応じて必要なパラメータを追加したコンストラクタを定義する
  • what関数をオーバーライドしてエラー情報を表すメッセージの出力をカスタマイズする
  • ファイルパス、ファイル行番号、関数名を取得するマクロを自動的に挿入するマクロ関数を定義する(★1)

★1 ファイルパス、ファイル行番号、関数名を取得するマクロはコンパイラが提供しており、それぞれ以下のマクロになります。

マクロ名 機能 備考
FILE ファイルパス(絶対パス)の取得
LINE ファイル行番号の取得
FUNCTION 関数名の取得
AppExceptionクラス

AppExceptionクラスは、汎用的に使用できる例外クラスで、ExceptionBaseをそのまま継承したクラスになります。

【AppException.h】

#pragma once
#include "ExceptionBase.h"

#include <vector>

namespace exception
{
    /* Application Exceptionクラス宣言 */
    class AppException : public ExceptionBase
    {
    public:
        /* コンストラクタ */
        AppException(const std::string& message);

        /* コンストラクタ */
        AppException(const std::string& message, const std::string& file, const std::string& func, const int line);

        /* デストラクタ */
        virtual ~AppException();
    };
}

/* Application Exceptionのthrow */
#define THROW_APP_EXCEPTION(message) \
    throw exception::AppException(message, __FILE__, __FUNCTION__, __LINE__)


【AppException.cpp】

#include "AppException.h"

namespace exception
{
    /* コンストラクタ */
    AppException::AppException(const std::string& message)
        : ExceptionBase(message)
    {
        /* Nothing to do */
    }

    /* コンストラクタ */
    AppException::AppException(const std::string& message, const std::string& file, const std::string& func, const int line)
        : ExceptionBase(message, file, func, line)
    {
        /* Nothing to do */
    }

    /* デストラクタ */
    AppException::~AppException()
    {
        /* Nothing to do */
    }
}


使用方法としては、エラー発生時に、マクロ関数THROW_APP_EXCEPTIONを呼び出すことで、呼び出し箇所のファイルパス、関数名、ファイル行番号を表すマクロが自動的に引数として渡され、エラー情報として格納されます。

そして例外をcatchしたときにwhat関数を呼び出すことで、エラー発生箇所の情報が付加されたエラーメッセージを取得することが出来ます。

【Test.cpp】

#include <iostream>
#include "StringFormat.h"
#include "AppException.h"

static void ExceptionTest();

static void TestFunc1();
static void TestFunc2();

int main()
{
    try
    {
        ExceptionTest();
    }
    catch (std::exception& ex)
    {
        std::cerr << StringFormat("[ERROR] %s", ex.what()) << std::endl;
    }
}

static void ExceptionTest()
{
    TestFunc1();
}

static void TestFunc1()
{
    TestFunc2();
}

static void TestFunc2()
{
    THROW_APP_EXCEPTION("Invoke Application Exception");
}


上記のような例の場合、実行結果は以下のようになります。

[ERROR] [Application Error] Invoke Application Exception @ TestFunc2[/<省略>/Test.cpp:L.34]


SocketExceptionクラス

SocketExceptionクラスは、ソケット通信関連処理で発生したエラーを通知する例外クラスで、ソケットAPIのエラーコードを保持することが出来ます。

【SocketException.h】

#pragma once
#include "ExceptionBase.h"

namespace exception
{
    /* Socket Exceptionクラス宣言 */
    class SocketException : public ExceptionBase
    {
    public:
        /* コンストラクタ */
        SocketException(const std::string& message, const int error_code);

        /* コンストラクタ */
        SocketException(const std::string& message, const int error_code, const std::string& file, const std::string& func, const int line);

        /* デストラクタ */
        virtual ~SocketException();

        /* エラーコードを取得 */
        int GetErrorCode();

        /* エラー要因を取得 */
        virtual char const* what() const noexcept override;

    private:
        /* エラーメッセージ生成 */
        const std::string GenerateErrorMessage();

    private:
        /* エラーコード */
        int m_ErrorCode;

        /* エラーメッセージ */
        std::string m_ErrorMessage;
    };
}

/* Socket Exceptionのthrow */
#define THROW_SOCKET_EXCEPTION(message, error_code) \
    throw exception::SocketException(message, error_code, __FILE__, __FUNCTION__, __LINE__)


【SocketException.cpp】

#include "SocketException.h"
#include "StringFormat.h"

namespace exception
{
    /* コンストラクタ */
    SocketException::SocketException(const std::string& message, const int error_code)
        : ExceptionBase(message)
        , m_ErrorCode(error_code)
        , m_ErrorMessage("")
    {
        /* エラーメッセージ生成 */
        this->m_ErrorMessage = this->GenerateErrorMessage();
    }

    /* コンストラクタ */
    SocketException::SocketException(const std::string& message, const int error_code, const std::string& file, const std::string& func, const int line)
        : ExceptionBase(message, file, func, line)
        , m_ErrorCode(error_code)
        , m_ErrorMessage("")
    {
        /* エラーメッセージ生成 */
        this->m_ErrorMessage = this->GenerateErrorMessage();
    }

    /* デストラクタ */
    SocketException::~SocketException()
    {
        /* Nothing to do */
    }

    /* エラーコードを取得 */
    int SocketException::GetErrorCode()
    {
        return this->m_ErrorCode;
    }

    /* エラー要因を取得 */
    char const* SocketException::what() const noexcept
    {
        return this->m_ErrorMessage.c_str();
    }

    /* エラーメッセージ生成 */
    const std::string SocketException::GenerateErrorMessage()
    {
        std::stringstream ss;

        /* エラー情報がある場合は、エラー情報付きメッセージを生成 */
        if (this->m_IsErrorInfoExists == true)
        {
            ss << StringFormat("[Socket Error] %s : Error Code = %d @ %s[%s:L.%d]", this->m_Message, this->m_ErrorCode, this->m_FunctionName, this->m_FilePath, this->m_LineNumber);
        }
        else
        {
            ss << StringFormat("[Socket Error] %s : Error Code = %d", this->m_Message, this->m_ErrorCode);
        }

        ss << std::endl;

        return ss.str();
    }
}


使用方法としては、AppExceptionクラスと同様、エラー発生時にマクロ関数THROW_SOCKET_EXCEPTIONを呼び出すことで、呼び出し箇所のファイルパス、関数名、ファイル行番号を表すマクロが自動的に引数として渡され、エラー情報として格納されます。

その際に、ソケットAPI失敗時のエラーコードも引数として渡すことが出来ます。

そして例外をcatchしたときにwhat関数を呼び出すことで、エラー発生箇所の情報とエラーコードが付加されたエラーメッセージを取得することが出来ます。

/* UDPユニキャスト送信用ソケットオープン */
void OpenUdpUniTxSocket(const std::string& remote_ip, const uint16_t remote_port)
{
    /* UDP用ソケットをオープン */
    int socket = socket(AF_INET, SOCK_DGRAM, 0);

    /* ソケットオープン失敗時のエラー処理 */
    if (socket < 0)
    {
        /* ソケット例外送出 */
        THROW_SOCKET_EXCEPTION("UDP Unicast Tx Socket Open Failed", errno);
    }

    /* 〜省略〜 */
}


あとがき

長くなったので今回はここまでにして、次回はスタックトレースを取得する方法を実装したいと思います。