[ C++で開発 ]

例外処理プログラミング

戻り値によるエラー通知ではなく、より障害に対する耐性を高めるためのシステムとしてC++言語に導入されたのが例外機構です。この例外処理はC++に限らずオブジェクト指向言語の多くに導入されています。

throwとcatchの書き方

3つの方法:ポインタ渡しか値渡しか参照渡しか

(本節の記述は、書籍「More Effective C++」の§13を参考としています)

C++でスローされた例外をcatchするとき、例外オブジェクトを受け取る引数型指定には3つの方法があります。

  1. ポインタ渡しで受け取る
  2. 値渡しで受け取る
  3. 参照渡しで受け取る

もっとも適切な方法は3.の参照渡しです。

No. 方法 問題・制約
1 ポインタ渡しで受け取る
class Exception {
    virtual const char* what() throw();
};

void maybeThrowFunction() {
    static Exception ex;
    throw &ex;
}

void catchFuntion() {
    try {
        maybeThrowFunction();
    } catch (Exception* ex) {
        ...
    }
}
ポインタの指す例外オブジェクト(例:ex)が、スコープ(例:関数throwFunc)を脱しても存在することを保証しなければ、破棄された例外オブジェクトをcatchしてしまう。(例では、static宣言をして関数スコープを逸脱しても存在を保証している。)

ポインタで受け取った例外オブジェクトは誰が破棄するのか不透明である。(結果としてメモリリークに陥りやすい)
2 値渡しで受け取る
class RuntimeError : public Exception {};

void maybeThrowFunction2() {
    throw RuntimeError();
}

void catchFuntion() {
    try {
        throwFunction();
    } catch (Exception ex) {
        cerr << ex.what() << endl;
    }
}
1回の例外発生につき、2回例外オブジェクトの生成処理が発生する。(throw文での生成、catch文でのコピー)

スライス問題が発生し、派生型の情報が欠落した例外オブジェクトをcatch文で受け取ってしまう。
(ポリモーフィズムが機能しない)
3 参照渡しで受け取る
void catchFuntion() {
    try {
        maybeThrowFunction2();
    } catch (const Exception& ex) {
        cerr << ex.what() << endl;
    }
}
例外オブジェクトの生成、スライス問題ともに影響なし。

テンポラリオブジェクトをconstでない参照引数へ渡すことは関数呼び出しでは許されないが例外catchでは許される。

アサーションが検出されると、アサーションの式と発生したファイル名、行番号をエラー表示してコアダンプします。

関数宣言のthrow:例外仕様

 関数宣言においてthrow()のなかに型を記述すると、その関数はthrow()内に記述した型以外を決してスローしないことを例外仕様として定義することになります。

 もし、例外仕様に違反する型を関数内からスローしようとすると、C++ランタイムシステムは、std::unexpected()関数を呼び出します。デフォルトのunexpected()の振る舞いは、terminate()の呼び出しです。デフォルトのterminate()の振る舞いは、メインスレッドに対してabort()を呼び出します。

 また、誰も例外をcatchしなかった場合は、terminate()が呼び出されるようです。

非メインスレッドでの振舞い、コンパイラ固有の振る舞いの違いについては要調査

unexpectedとterminateの振る舞いは、set_unexpectedおよびset_terminateの関数で自前の関数ポインタを登録することで変更することができます。

単純なunexpected/terminateの実装

void myUnexpectedHandler() {
    std::cerr << "myUnexpectedHandler called." << std::endl;
    terminate();
}

void myTerminateHandler() {
    std::cerr << "myTerminateHandler called." << std::endl;
    abort;
}

int main(int argc, char* argv[]) {
    std::set_unexpected(myUnexpectedHandler);
    std::set_terminate(myTerminateHandler);
    ...
}

例外の考え方

「どのようなときに例外を使うか?」についての一つの考え方です。

例外を使用する/しない

通常の処理フローで発生する結果については、例外は使用しません。

例をいくつか挙げます。

例外処理をする/しない

ある関数を呼び出したとき、その関数処理内で例外が発生する可能性がある場合、必ず例外処理(try-catch)を書かなくてはならないのでしょうか?

やってはいけないこと

スタックフレームを保持する例外

 例外が発生したとき、すなわち例外インスタンスが生成された時点のスタックフレーム情報とファイル名、関数名、ソースコードの行番号を保持する例外クラスを作成します。しかし、スタックフレーム情報の取得が不要な例外もあると想定し、ファイル名、関数名、行番号を保持するExceptionクラスと、スタックフレーム情報をさらに追加したTraceExceptionクラスを作成しました。

 使い方のイメージは、

void causingException() {
    ...
    THROW_EXCEPTION(exception::TraceException, "Resource not exist");
}

void exceptionalMethod(int anInput) {
    // ...
    try {
        causingException();
    } catch (TraceException& ex) {
        logError(ex);
    }
}

Exceptionクラス

C++標準例外 std::exceptionを継承します。

あとは、メンバ変数に例外メッセージ、ファイル名、関数名、行番号を持ち、そのアクセッサメンバ関数を定義した内容です。

(基底)例外 Exception
#pragma once

#include <exception>
#include <string>

#define THROW_EXCEPTION(EXCEPTION_TYPE, message) \
  throw EXCEPTION_TYPE(message, __FILE__, __func__, __LINE__)  

namespace exception {
class Exception : public std::exception {
public:
    Exception(const std::string& aMessage);
    Exception(const std::string& aMessage,
              const char* aFile,
              const char* aFuncion,
              const int aLine);
    virtual ~Exception() throw();
    const char* getFileName() const;
    const char* getFunctionName() const;
    const int getLineNumber() const;
    virtual const char* what() const throw();

private:
    std::string message;
    const char* fileName;
    const char* functionName;
    int line;
};

}        

実装コードは

Exception.cpp
#include "exception/Exception.h"

namespace exception {

Exception::Exception(const std::string& aMessage)
 : message(aMessage)
{}

Exception::Exception(const std::string& aMessage, const char* aFile,
           const char* aFunction, const int aLine)
 : message(aMessage), fileName(aFile), functionName(aFunction), line aLine) 
{}

Exception::~Exception() throw() {}

const char* Exception::what() const throw() {
    return message.c_str();
}

const char* Exception::getFileName() const {
        return fileName;
}

const char* Exception::getFunctionName() const {
        return functionName;
}

const int Exception::getLineNumber() const {
        return line;
}
}                    

TraceExceptionクラス

スタックフレーム情報を取得する方法は、プラットフォーム・コンパイラ固有の処理となります。

Linuxのbacktraceシステムコール

以下ヘッダーファイルをインクルードします。

#include <execinfo.h>

バックトレース情報の取得は backtrace 関数を使用します。

int backtrace(void** buffer, int size);

backtrace の使用例

const int maxTraces = 1024; // 格納するスタックフレームの最大個数
void* traceBuffers[maxTraces]; // スタックフレームへのアドレスを格納

int numTraces = backtrace(traceBuffers, maxTraces);

backtrace で取得できる情報は、スタックフレームへのアドレスです。実際に欲しい情報は関数名なので、アドレスから関数名に変換する必要があります。この変換には、backtrace_symbols システムコールを利用します。

char** backtrace_symbols(void* const* buffer, int size);

backtrace_symbols の使用例

char** traceStrings = backtrace_symbols(traceBuffers, numTraces);
if (traceStrings == 0) {
    // エラー処理
}
for (int i=0; i<numTraces; ++i) {
    std::cerr << strings[i] << std::endl;
}
free(strings);  // backtrace_symbolsの戻り値は呼び出し側で解放すること

なお、ファイルディスクリプタにスタック情報を出力するための、backtrace_symbols_fd システムコールもあります。

void backtrace_symbols_fd(void* const* buffer, int size, int fd);

backtraceを用いた例外クラスの実装例

以下は、Linuxのシステムライブラリ(glibc)で用意されるbacktraceを使用して実装した例です。

TraceException.h
#pragma once

#include <string>
#include "exception/Exception.h"    

namespace exception {

const int MAX_TRACE = 256;

class TraceException : public Exception {
    void* traces[MAX_TRACE];
    int tracesSize;
    char** symbols;
    void init();

public:
    TraceException(const std::string& aMessage);
    TraceException(const std::string& aMessage, const char* aFile,
                   const char* aFunction, const int aLine);
    void printStackTrace() const;
};

}   

実装は、

#include "exception/TraceException.h"
#include <iostream>
#include <execinfo.h>

namespace exception {

TraceException::TraceException(const std::string& aMessage)
 : Exception(aMmessage) {
        init();
}

TraceException::TraceException(const std::string& a_message,
                               const char* aFile, const char* aFunction,
                               const int aLine)
 : Exception(aMessage, aFile, aFunction, aLine) {
        init();
}

void TraceException::printStackTrace() const {
    std::cerr.setf(std::ios::dec, std::ios::basefield);
    std::cerr << "TraceException: " << what() << "@" << getFileName() << "::"
             << getFunctionName() << "(" << getLineNumber() << ")" << std::endl;
    int status;
    for (int i=0; i<tracesSize; i++) {
        std::cerr.setf(std::ios::hex, std::ios::basefield);
        std::cerr.setf(std::ios::showbase);
        std::cerr << traces[i] << " | " << symbols[i] << std::endl;
    }
}

void TraceException::init() {
    tracesSize = backtrace(traces, MAX_TRACE);
    symbols = backtrace_symbols(traces, tracesSize);
}

}