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;;
    }
}

対応していないと…


  1. try/throw/catch の基本とcatch順序
  2. 伝播と再スロー(どの層で決着させるか)
  3. RAII (ライ)による自動後始末(例外時も安全) 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. よくある落とし穴チェックリスト


理解度チェック(四肢択一・IPA風)

問1(23_q1.cpp) catch の並びとして正しいものを選べ。

catch(const exception&) → catch(const out_of_range&) → catch(...)

catch(const out_of_range&) → catch(const exception&) → catch(...)

catch(...) → catch(const runtime_error&) → catch(const exception&)

catch(const exception&) → catch(const runtime_error&) → catch(const out_of_range&)

正解・解説 イ。具体(`out_of_range`)→ 抽象(`exception`)→ 万能(`...`)。

問2(23_q2.cpp) 再スロー(throw;)について正しい説明を選べ。

throw e;throw; は常に同じ挙動である。

throw;直近で捕捉した例外をそのまま再スローする。

throw; は新しい exception() を投げ直す。

throw;try の外でも使える。

正解・解説 イ。`throw;` は直前の `catch` 範囲内でのみ有効。オブジェクトを書かないことで**元の例外**を保ったまま再スローする。

問3(23_q3.cpp) 例外安全の観点で望ましい選択を選べ。

new で配列確保し、例外時は手作業で delete[]

vector<Enemy*>new で格納し、終了時に delete をループで実行。

vector<Enemy> もしくは vector<unique_ptr<Enemy>> を使う。

エ 例外は使わず exit(1) で終了する。

正解・解説 ウ。RAIIにより**自動解放**が保証され、例外時もリークや二重解放を防ぎやすい。

問4(23_q4.cpp) 次の divide はどのように改善すべきか。

int divide(int a, int b)
{
    if (b == 0)
    {
        // TODO: 改善
    }
    return a / b;
}

return 0; にしておけば安全。

throw runtime_error("0で割れない"); として呼び出し側で catch

cout << "エラー"; を出して続行。

assert(b != 0); のみ。

正解・解説 イ。**呼び出し側に判断を委ねる**のが自然。`assert` は開発時向け、`return 0` はサイレント障害の温床。

演習(提出課題)

演習1(23_ex1_guarded_load.cpp)

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

演習2(23_ex2_custom_error.cpp)

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

演習3(23_ex3_rethrow_chain.cpp)

目的: 伝播と再スローのパターンを体験。

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


まとめ