23:エラー処理と例外(try, throw, catch)

はじめに

例外対処の基本パターン

// 23_intro_quick.cpp
#include <iostream>
#include <stdexcept>
using namespace std;

int divi(int a, int b)
{
    if (b == 0)
    {
        throw runtime_error("0で割れない");
    }
    return a / b;
}

int main(void)
{
    try
    {
        cout << divi(10, 2) << endl;
        cout << divi(5, 0) << endl; // ここで例外
    }
    catch (const runtime_error &e)
    {
        cout << "エラー: " << e.what() << endl;
    }
}
ポイント:throw(投げる)で知らせ、try/catch(試みる/受け取る)で受けて落ちずに処理を制御する。
対応していないと…

この資料で学ぶこと

  1. try/throw/catch の基本と catch順序
  2. 伝播と再スロー(どの層で決着させるか)
  3. RAII による自動後始末(例外時も安全)
  4. 独自例外と標準系の使い分け
  5. VSデバッガでスローの瞬間を観察

1. 基本構文(23_try_basic.cpp)

// 23_try_basic.cpp
#include <iostream>
#include <stdexcept>
using namespace std;

int divide(int a, int b)
{
    if (b == 0)
    {
        throw runtime_error("0で割ることはできません");
    }
    return a / b;
}

int main(void)
{
    try
    {
        cout << divide(10, 2) << endl;
        cout << divide(5, 0) << endl; // ここで例外
        cout << "ここには到達しません\n";
    }
    catch (const runtime_error &e)
    {
        cout << "実行時エラー: " << e.what() << endl;
    }
    catch (...)
    {
        cout << "想定外の例外を捕捉" << endl;
    }
    return 0;
}
ポイント

2. 複数catchと順序(23_catch_order.cpp)

// 23_catch_order.cpp
#include <iostream>
#include <stdexcept>
using namespace std;

void f(int code)
{
    if (code == 1)
    {
        throw out_of_range("範囲外");
    }
    else if (code == 2)
    {
        throw runtime_error("実行時");
    }
    else
    {
        throw exception();
    }
}

int main(void)
{
    try
    {
        f(1); // 1,2,その他 を変えて試す
    }
    catch (const out_of_range &e)
    {
        cout << "out_of_range: " << e.what()  << endl;
    }
    catch (const runtime_error &e)
    {
        cout << "runtime_error: " << e.what() << endl;
    }
    catch (const exception &e)
    {
        cout << "exception: " << e.what() << endl;
    }
}
ポイント

3. 例外の伝播と再スロー(23_rethrow.cpp)

// 23_rethrow.cpp
#include <iostream>
#include <stdexcept>
using namespace std;

void low()
{
    throw runtime_error("lowで失敗");
}

void mid()
{
    try
    {
        low();
    }
    catch (const runtime_error &)
    {
        // ログだけ取り、呼び出し元へそのまま再スロー
        throw;
    }
}

int main(void)
{
    try
    {
        mid();
    }
    catch (const exception &e)
    {
        cout << "mainで捕捉: " << e.what() << endl;
    }
}
ポイント

4. RAIIと例外安全(23_raii_safety.cpp)

// 23_raii_safety.cpp
#include <iostream>
#include <vector>
#include <memory>
using namespace std;

class Enemy
{
public:
    explicit Enemy(int hp) : hp_(hp) {}
    int hp() const { return hp_; }
private:
    int hp_;
};

int main(void)
{
    // 例外が起きても自動で後始末される安全な資源管理
    vector<unique_ptr<Enemy>> es;
    es.push_back(make_unique<Enemy>(10));
    es.push_back(make_unique<Enemy>(20));

    // 途中で例外が起きてもOK(ここでは例として投げる)
    throw runtime_error("途中で失敗");

    // 到達しないが、esはスコープ終了時に自動解放される
}
ポイント

5. 独自例外クラス(23_custom_exception.cpp)

// 23_custom_exception.cpp
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;

class InvalidHpError : public runtime_error
{
public:
    explicit InvalidHpError(const string &msg) : runtime_error(msg) {}
};

class Enemy
{
public:
    explicit Enemy(int hp)
    {
        if (hp < 0)
        {
            throw InvalidHpError("HPが負です: " + to_string(hp));
        }
        hp_ = hp;
    }
private:
    int hp_ = 0;
};

int main(void)
{
    try
    {
        Enemy e(-5);
    }
    catch (const InvalidHpError &e)
    {
        cout << "InvalidHpError: " << e.what() << endl;
    }
}
ポイント

6. Visual Studioで「例外の瞬間」を見る

  1. 本章いずれかのソースをデバッグ実行(F5)
  2. 例外がスローされる行にブレーク(自動で止まる or 自分でブレークポイントを設定)。
  3. 呼び出し履歴(Call Stack)で伝播の経路を確認。
  4. ローカル/自動ウィンドウで変数の値を観察。
  5. catch の中にブレークポイントを置き、捕捉→継続の流れを確認。
体験タスク:23_rethrow.cpp を使い、low → mid → main の順に例外が通過する様子を一歩ずつ観察する。

7. よくある落とし穴チェックリスト

演習(提出課題)

演習1(23_ex1_guarded_load.cpp)

目的:リソース読み込み失敗を例外で扱い、安全に後始末する。

演習2(23_ex2_custom_error.cpp)

目的独自例外で入力不正を表現。

演習3(23_ex3_rethrow_chain.cpp)

目的伝播と再スローのパターンを体験。 提出物:ソース一式と実行ログ。どこで何を捕捉し、どのように再スローしたかをコメントで説明すること。

コーディング演習

演習1:基本コードを動かす

23_01_try_basic.cpp を作成し、基本構文のコードを入力して実行しましょう。divide(5, 0) の呼び出しで例外が発生し、catch に飛ぶことを確認してください。

演習2:コードを改造する

23_01_TryCatch23_try_basic.cpp をコピーして 23_02_TryCatchMod に貼り付け、次の変更を加えてみましょう:

まとめ

理解度チェック

問題23-1

catch の並びとして正しいものを選べ。

解説を表示 正解:イ
具体(out_of_range)→ 抽象(exception)→ 万能(...)の順が正しい。基底クラスを先に書くと、派生クラスの例外もそこで捕まってしまい、より具体的な catch に到達できなくなる。

問題23-2

再スロー(throw;)について正しい説明を選べ。

解説を表示 正解:イ
throw; は直前の catch 範囲内でのみ有効。オブジェクトを書かないことで元の例外を保ったまま再スローする。throw e; はコピーして投げるので型情報が変わる可能性がある。

問題23-3

例外安全の観点で望ましい選択を選べ。

解説を表示 正解:ウ
RAIIにより自動解放が保証され、例外時もリークや二重解放を防ぎやすい。vector<Enemy*> の場合は自分で delete しなければならないので例外時にリークの危険がある。

問題23-4

次の divide 関数はどのように改善すべきか。

int divide(int a, int b)
{
    if (b == 0)
    {
        // TODO: 改善
    }
    return a / b;
}
解説を表示 正解:イ
呼び出し側に判断を委ねるのが自然。assert は開発時向け(リリースビルドでは無効化される)、return 0 はサイレント障害の温床、メッセージを出して続行は誤結果を生む。