例外対処の基本パターン
// 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;
}
}
try/throw/catch の基本と catch順序// 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;
}
throw で例外オブジェクトを投げる。catch (型) はより具体的な型から並べる(順序が大事)。catch (...) はどんな型でも受け止める保険。// 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;
}
}
std::exception は最後に。// 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;
}
}
throw;(オブジェクトを書かない)で元の例外を再スロー。// 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はスコープ終了時に自動解放される
}
new/delete は例外時にdelete漏れが起きやすい。vector や unique_ptr を使うのが基本方針。// 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;
}
}
std::logic_error / std::runtime_error など)を継承して意味を明確に。what() に詳細を入れて原因調査を早くする。catch の中にブレークポイントを置き、捕捉→継続の流れを確認。23_rethrow.cpp を使い、low → mid → main の順に例外が通過する様子を一歩ずつ観察する。
catch の順序:具体 → 抽象。new 後に例外 → delete 漏れ。→ vector とスマートポインタに置き換える。catch (...) {} の乱用。throw の多用。→ 事前条件チェックで分岐処理できるならそれを優先。loadTexture(const string&) を作成。拡張子が .png 以外なら invalid_argument を throw。main では vector<unique_ptr<int>> など簡易なリソースを確保後、わざと loadTexture("enemy.dat") を呼び例外を発生させる。catch でメッセージ表示。終了時に確保済み資源が自動解放されることを確認。class InvalidParam : public runtime_error を作る。Enemy のコンストラクタで hp < 0 または name.empty() なら InvalidParam を投げる。try 範囲で複数回生成を試し、catch(const InvalidParam&) で個別メッセージを表示。low() → mid() → main() の三層構造。low() で runtime_error を投げ、mid() でログだけ出して throw;。main() で最終捕捉し、ユーザー向けメッセージを表示。divide(5, 0) の呼び出しで例外が発生し、catch に飛ぶことを確認してください。
23_01_TryCatch の 23_try_basic.cpp をコピーして 23_02_TryCatchMod に貼り付け、次の変更を加えてみましょう:
DivisionByZeroError を runtime_error を継承して作成するdivide() 関数で DivisionByZeroError を投げるように変更するcatch で DivisionByZeroError と exception の2種類を受けるようにするcatch は具体 → 抽象。catch(...) は最後の保険。new/delete を直接使わない設計を基本に。what() を有益にする。catch の並びとして正しいものを選べ。
out_of_range)→ 抽象(exception)→ 万能(...)の順が正しい。基底クラスを先に書くと、派生クラスの例外もそこで捕まってしまい、より具体的な catch に到達できなくなる。
再スロー(throw;)について正しい説明を選べ。
throw; は直前の catch 範囲内でのみ有効。オブジェクトを書かないことで元の例外を保ったまま再スローする。throw e; はコピーして投げるので型情報が変わる可能性がある。
例外安全の観点で望ましい選択を選べ。
vector<Enemy*> の場合は自分で delete しなければならないので例外時にリークの危険がある。
次の divide 関数はどのように改善すべきか。
int divide(int a, int b)
{
if (b == 0)
{
// TODO: 改善
}
return a / b;
}
assert は開発時向け(リリースビルドでは無効化される)、return 0 はサイレント障害の温床、メッセージを出して続行は誤結果を生む。