第10章:例外処理

はじめに

例外処理は、エラーが発生した場所とそれを処理する場所を分離するメカニズムです。この概念は、CLU(1975年)で最初に導入され、C++は1990年に例外処理を採用しました。

本章では、例外処理の理論的背景から、C++での実践的な使用方法、そして例外安全性について学びます。

---

1. 例外処理の概念

1.1 なぜ例外処理が必要か

従来のエラー処理方法には問題がありました:

戻り値によるエラー通知

// C言語スタイル
FILE* file = fopen("data.txt", "r");
if (file == NULL) {
    // エラー処理
    return -1;
}
// ファイル処理...

問題点:

  • 戻り値がエラーと正常値で競合する
  • エラーチェックを忘れやすい
  • エラー処理コードが本質的なコードに混在

例外処理のアプローチ

// C++スタイル
try {
    std::ifstream file("data.txt");
    if (!file) {
        throw std::runtime_error("Failed to open file");
    }
    // ファイル処理...
} catch (const std::runtime_error& e) {
    std::cerr << "Error: " << e.what() << std::endl;
}

利点:

  • エラー処理と正常処理の分離
  • エラーを無視しにくい
  • スタックの巻き戻しでリソースが自動解放(RAII)

1.2 例外の歴史

| 年 | 言語/出来事 | 内容 | |-----|------------|------| | 1975 | CLU | 例外処理の概念を導入 | | 1985 | C++ | 当初は例外なし | | 1990 | C++ 2.0 | 例外処理を追加 | | 1998 | C++98 | 例外仕様(throw()) | | 2011 | C++11 | noexcept導入、例外仕様非推奨化 |

1.3 基本的な構文

// 例外を投げる
void riskyFunction() {
    if (/* エラー条件 */) {
        throw std::runtime_error("Something went wrong");
    }
}

// 例外をキャッチする
int main() {
    try {
        riskyFunction();
        // 正常処理
    } catch (const std::runtime_error& e) {
        std::cerr << "Runtime error: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    } catch (...) {
        std::cerr << "Unknown exception" << std::endl;
    }
    return 0;
}

---

2. 標準例外クラス

2.1 例外階層

C++標準ライブラリの例外階層:

std::exception
├── std::logic_error
│   ├── std::invalid_argument
│   ├── std::domain_error
│   ├── std::length_error
│   └── std::out_of_range
│
├── std::runtime_error
│   ├── std::range_error
│   ├── std::overflow_error
│   └── std::underflow_error
│
├── std::bad_alloc          (new失敗)
├── std::bad_cast           (dynamic_cast失敗)
├── std::bad_typeid         (typeid失敗)
└── std::bad_exception

2.2 各例外クラスの使い分け

#include <stdexcept>

void validateAge(int age) {
    if (age < 0 || age > 150) {
        throw std::invalid_argument("Age must be between 0 and 150");
    }
}

void accessElement(const std::vector<int>& vec, size_t index) {
    if (index >= vec.size()) {
        throw std::out_of_range("Index out of range");
    }
}

void divideNumbers(int a, int b) {
    if (b == 0) {
        throw std::domain_error("Division by zero");
    }
}

void checkOverflow(long result) {
    if (result > INT_MAX) {
        throw std::overflow_error("Integer overflow");
    }
}

int main() {
    try {
        validateAge(-5);
    } catch (const std::invalid_argument& e) {
        std::cerr << "Invalid argument: " << e.what() << std::endl;
    }

    try {
        std::vector<int> v = {1, 2, 3};
        accessElement(v, 10);
    } catch (const std::out_of_range& e) {
        std::cerr << "Out of range: " << e.what() << std::endl;
    }

    return 0;
}

2.3 std::exceptionの使い方

void processAll() {
    try {
        // 様々な例外が発生する可能性
    } catch (const std::logic_error& e) {
        // プログラムの論理エラー
        std::cerr << "Logic error: " << e.what() << std::endl;
    } catch (const std::runtime_error& e) {
        // 実行時エラー
        std::cerr << "Runtime error: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        // その他の標準例外
        std::cerr << "Exception: " << e.what() << std::endl;
    } catch (...) {
        // 非標準の例外
        std::cerr << "Unknown exception" << std::endl;
    }
}

---

3. カスタム例外クラス

3.1 基本的なカスタム例外

class FileNotFoundException : public std::runtime_error {
public:
    FileNotFoundException(const std::string& filename)
        : std::runtime_error("File not found: " + filename),
          filename_(filename) {}

    const std::string& getFilename() const { return filename_; }

private:
    std::string filename_;
};

class DatabaseException : public std::runtime_error {
public:
    DatabaseException(const std::string& message, int errorCode)
        : std::runtime_error(message),
          errorCode_(errorCode) {}

    int getErrorCode() const { return errorCode_; }

private:
    int errorCode_;
};

void openFile(const std::string& filename) {
    std::ifstream file(filename);
    if (!file) {
        throw FileNotFoundException(filename);
    }
}

int main() {
    try {
        openFile("nonexistent.txt");
    } catch (const FileNotFoundException& e) {
        std::cerr << e.what() << std::endl;
        std::cerr << "Filename: " << e.getFilename() << std::endl;
    }
    return 0;
}

3.2 例外クラスのベストプラクティス

// ベストプラクティスに従った例外クラス
class ApplicationException : public std::exception {
public:
    explicit ApplicationException(const std::string& message)
        : message_(message) {}

    // 例外安全なwhat()の実装
    const char* what() const noexcept override {
        return message_.c_str();
    }

    // コピー可能で例外を投げない
    ApplicationException(const ApplicationException&) noexcept = default;
    ApplicationException& operator=(const ApplicationException&) noexcept = default;

    // 仮想デストラクタ
    virtual ~ApplicationException() noexcept = default;

private:
    std::string message_;
};

// 派生例外クラス
class NetworkException : public ApplicationException {
public:
    NetworkException(const std::string& message, int errorCode)
        : ApplicationException(message + " (Error: " + std::to_string(errorCode) + ")"),
          errorCode_(errorCode) {}

    int getErrorCode() const noexcept { return errorCode_; }

private:
    int errorCode_;
};

3.3 例外の再投げ

void lowLevelFunction() {
    throw std::runtime_error("Low level error");
}

void midLevelFunction() {
    try {
        lowLevelFunction();
    } catch (const std::exception& e) {
        // ログを取る
        std::cerr << "Caught in mid level: " << e.what() << std::endl;

        // 同じ例外を再投げ
        throw;  // throw e; ではない!スライシングを避ける
    }
}

void highLevelFunction() {
    try {
        midLevelFunction();
    } catch (const std::exception& e) {
        std::cerr << "Caught in high level: " << e.what() << std::endl;
    }
}

---

4. 例外安全性

4.1 例外安全性のレベル

例外安全性には3つのレベルがあります:

| レベル | 名前 | 保証 | |--------|------|------| | 1 | 基本保証 | リソースリークなし、不変条件維持 | | 2 | 強い保証 | 操作は成功するか、元の状態のまま | | 3 | nothrow保証 | 例外を投げない |

4.2 基本保証

リソースリークがなく、オブジェクトが有効な状態を維持:

class BasicGuarantee {
    std::vector<int> data;

public:
    void addElement(int value) {
        data.push_back(value);  // 例外が発生してもdataは有効
    }
};

4.3 強い保証

操作が失敗しても、元の状態が完全に保持される:

class StrongGuarantee {
    std::vector<int> data;

public:
    void replaceData(const std::vector<int>& newData) {
        std::vector<int> temp = newData;  // コピーを作成(ここで例外発生の可能性)
        temp.swap(data);  // swapはnoexcept
    }  // 成功すればnewDataで置換、失敗すれば元のまま
};

// コピーアンドスワップイディオム
class Resource {
    int* data;
    size_t size;

public:
    Resource(size_t n) : data(new int[n]()), size(n) {}

    Resource(const Resource& other)
        : data(new int[other.size]), size(other.size) {
        std::copy(other.data, other.data + size, data);
    }

    ~Resource() { delete[] data; }

    // 強い例外安全な代入演算子
    Resource& operator=(Resource other) {  // 値渡し(コピーコンストラクタ呼び出し)
        swap(other);  // noexcept
        return *this;
    }

    void swap(Resource& other) noexcept {
        std::swap(data, other.data);
        std::swap(size, other.size);
    }
};

4.4 nothrow保証

class NothrowGuarantee {
    int value;

public:
    // noexcept指定子
    int getValue() const noexcept {
        return value;
    }

    void setValue(int v) noexcept {
        value = v;
    }

    // ムーブ操作はnoexceptであるべき
    NothrowGuarantee(NothrowGuarantee&& other) noexcept
        : value(other.value) {}

    NothrowGuarantee& operator=(NothrowGuarantee&& other) noexcept {
        value = other.value;
        return *this;
    }
};

4.5 RAIIと例外安全性

RAIIはコンストラクタでリソースを取得し、デストラクタで解放するパターンです。例外発生時もデストラクタは呼ばれるため、リソースリークを防げます:

class Lock {
    std::mutex& mutex_;

public:
    explicit Lock(std::mutex& m) : mutex_(m) {
        mutex_.lock();
    }

    ~Lock() {
        mutex_.unlock();  // 例外発生時も確実にアンロック
    }

    // コピー禁止
    Lock(const Lock&) = delete;
    Lock& operator=(const Lock&) = delete;
};

void criticalSection(std::mutex& m) {
    Lock lock(m);  // ロック取得

    doSomethingThatMightThrow();  // 例外発生しても...

}  // 自動的にアンロック

---

5. noexceptキーワード(C++11)

5.1 noexcept指定子

関数が例外を投げないことを宣言:

// 例外を投げない関数
void safeFunction() noexcept {
    // 例外を投げないコード
}

// 条件付きnoexcept
template <typename T>
void conditionalNoexcept(T value) noexcept(std::is_nothrow_move_constructible<T>::value) {
    // Tのムーブがnoexceptならこの関数もnoexcept
}

// noexceptを違反した場合
void breakNoexcept() noexcept {
    throw std::runtime_error("Oops");  // std::terminateが呼ばれる!
}

5.2 noexcept演算子

式がnoexceptかどうかをコンパイル時に判定:

void mayThrow() { throw 1; }
void noThrow() noexcept {}

int main() {
    std::cout << std::boolalpha;
    std::cout << "mayThrow: " << noexcept(mayThrow()) << std::endl;  // false
    std::cout << "noThrow: " << noexcept(noThrow()) << std::endl;    // true

    // 算術演算はnoexcept
    std::cout << "1 + 2: " << noexcept(1 + 2) << std::endl;  // true

    return 0;
}

5.3 ムーブとnoexcept

ムーブ操作がnoexceptでないと、STLコンテナは効率的に動作しません:

class OptimalMove {
    int* data;

public:
    // noexceptなムーブコンストラクタ
    OptimalMove(OptimalMove&& other) noexcept
        : data(other.data) {
        other.data = nullptr;
    }

    // noexceptなムーブ代入演算子
    OptimalMove& operator=(OptimalMove&& other) noexcept {
        if (this != &other) {
            delete data;
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }
};

// vectorのreallocateでムーブが使われる
std::vector<OptimalMove> vec;
vec.push_back(OptimalMove());  // noexceptならムーブ、そうでなければコピー

---

6. 例外処理のベストプラクティス

6.1 投げる例外の選択

// 良い例:適切な例外タイプを選択
void validateInput(const std::string& input) {
    if (input.empty()) {
        throw std::invalid_argument("Input cannot be empty");
    }
}

// 悪い例:一般的すぎる例外
void badValidation(const std::string& input) {
    if (input.empty()) {
        throw std::exception();  // 情報が少なすぎる
    }
}

// 悪い例:非標準の例外
void veryBadValidation(const std::string& input) {
    if (input.empty()) {
        throw "Input cannot be empty";  // const char*を投げている
    }
}

6.2 例外のキャッチ方法

// 良い例:const参照でキャッチ
try {
    // ...
} catch (const std::exception& e) {
    std::cerr << e.what() << std::endl;
}

// 悪い例:値でキャッチ(スライシングの危険)
try {
    // ...
} catch (std::exception e) {  // コピーが発生、派生クラスの情報が失われる
    std::cerr << e.what() << std::endl;
}

6.3 デストラクタでの例外

デストラクタは例外を投げてはいけません:

class Resource {
public:
    ~Resource() noexcept {  // C++11以降、デストラクタは暗黙的にnoexcept
        try {
            cleanup();
        } catch (...) {
            // 例外を握りつぶすか、ログを取る
            std::cerr << "Cleanup failed" << std::endl;
        }
    }

private:
    void cleanup() {
        // 例外を投げる可能性のあるクリーンアップ処理
    }
};

6.4 例外と制御フロー

例外は例外的な状況に対してのみ使用:

// 悪い例:通常の制御フローに例外を使用
int findValue(const std::vector<int>& vec, int target) {
    for (size_t i = 0; i < vec.size(); ++i) {
        if (vec[i] == target) {
            throw i;  // 例外を戻り値代わりに使用
        }
    }
    throw std::runtime_error("Not found");
}

// 良い例:戻り値を使用
std::optional<size_t> findValue(const std::vector<int>& vec, int target) {
    for (size_t i = 0; i < vec.size(); ++i) {
        if (vec[i] == target) {
            return i;
        }
    }
    return std::nullopt;
}

---

7. 例外 vs エラーコード

7.1 比較

| 観点 | 例外 | エラーコード | |------|------|-------------| | エラー無視 | 困難(伝播する) | 容易(チェックを忘れやすい) | | パフォーマンス | 正常時は影響なし、異常時は遅い | 常に小さなオーバーヘッド | | コードの可読性 | エラー処理が分離 | 本質的なコードに混在 | | コンストラクタ | 使用可能 | 困難(戻り値がない) | | リソース管理 | RAIIで自動化 | 手動管理が必要 |

7.2 使い分けの指針

例外を使う場合

  • コンストラクタでのエラー
  • 深いコールスタックを越えたエラー伝播
  • エラーが例外的(まれ)な状況
  • RAIIを活用したい場合

エラーコードを使う場合

  • 頻繁に発生する「エラー」(入力検証など)
  • リアルタイムシステム
  • 例外が無効化されている環境
  • C言語との互換性が必要
  • 7.3 std::expected(C++23)

    例外とエラーコードの中間的なアプローチ:

    // C++23 std::expected
    #include <expected>
    
    std::expected<int, std::string> divide(int a, int b) {
        if (b == 0) {
            return std::unexpected("Division by zero");
        }
        return a / b;
    }
    
    int main() {
        auto result = divide(10, 2);
        if (result) {
            std::cout << "Result: " << *result << std::endl;
        } else {
            std::cout << "Error: " << result.error() << std::endl;
        }
        return 0;
    }
    

    ---

    まとめ

    本章で学んだこと:

  • 例外処理の概念: エラー処理の分離、歴史
  • 標準例外クラス: 例外階層、使い分け
  • カスタム例外: 設計のベストプラクティス
  • 例外安全性: 3つのレベル、コピーアンドスワップ
  • noexcept: 指定子と演算子
  • ベストプラクティス: 投げ方、キャッチ方法、デストラクタ
  • 例外 vs エラーコード: 使い分けの指針
  • 次章では、モダンC++(C++11以降)の機能について学びます。

    ---

    練習問題

  • 以下の関数を例外安全(強い保証)に書き換えてください:
void transfer(BankAccount& from, BankAccount& to, double amount) {
    from.withdraw(amount);  // 例外を投げる可能性
    to.deposit(amount);     // 例外を投げる可能性
}

  • カスタム例外クラスParseExceptionを作成してください。以下の情報を含むこと:
- エラーメッセージ - エラーが発生した行番号 - エラーが発生した列番号

  • 以下のコードの例外安全性の問題を指摘してください:
class Widget {
    int* data1;
    int* data2;
public:
    Widget() : data1(new int(1)), data2(new int(2)) {}
    ~Widget() { delete data1; delete data2; }
};

  • noexceptを適切に使用して、以下のクラスを改善してください:
class Buffer {
    char* data;
    size_t size;
public:
    Buffer(size_t n);
    Buffer(const Buffer& other);
    Buffer(Buffer&& other);
    Buffer& operator=(const Buffer& other);
    Buffer& operator=(Buffer&& other);
    ~Buffer();
    void swap(Buffer& other);
};