プログラムが扱うデータは常に正しいとは限りません。
ファイルが存在しない、ゼロ除算が起きた、配列の範囲外にアクセスした――こうした事象は、通常の処理の流れでは対処できない「例外的な事態」です。
C++では、この“異常事態”を通知する仕組みとして 例外(exception) が用意されています。
例外処理を使うことで、通常のロジックとエラー処理を分離し、コードの保守性と信頼性を高めることができます。
本記事ではまず、if分岐によるエラー処理を示し、その後に try/throw/catch による例外処理へ書き換えた例を比較しながら、例外処理の基本を解説します。
1. 分岐によるエラー処理(例外を使わない方法)
まずは、典型的な「戻り値でエラーを通知する」スタイルのコードです。
#include <iostream>
#include <vector>
// valueをチェックし0以上なら2倍した値をresultに入れてtrueを返す
bool process(int value, int& result) {
if (value < 0) {
std::cerr << "エラー: 負の値は処理できません(値: " << value << ")\n";
return false;
}
result = value * 2;
return true;
}
int main() {
std::vector<int> data = {1, 2, -3, 4, -5};
for (int i = 0; i < data.size(); ++i) {
int output;
if (!process(data[i], output)) {
std::cerr << "処理失敗(index: " << i << ")\n";
continue;
}
std::cout << "処理成功: " << output << "\n";
}
return 0;
}
● このコードのポイント
-
process()は 負の値を拒否し、false を返す - 呼び出し側は if文で毎回エラー判定
- 正常系と異常系が混在し、ロジックがやや読みにくい
● 実行結果
処理成功: 2
処理成功: 4
エラー: 負の値は処理できません(値: -3)
処理失敗(index: 2)
処理成功: 8
エラー: 負の値は処理できません(値: -5)
処理失敗(index: 4)
このように、戻り値でエラーを通知する方法はシンプルですが、呼び出し側のコードが煩雑になりやすいという欠点があります。
2. try/throw/catch による例外処理
次に、同じ処理を 例外処理 を使って書き直したコードです。
#include <iostream>
#include <vector>
#include <stdexcept>
int process(int value) {
if (value < 0) {
throw std::invalid_argument("負の値は処理できません(値: " + std::to_string(value) + ")");
}
return value * 2;
}
int main() {
std::vector<int> data = {1, 2, -3, 4, -5};
for (int i = 0; i < data.size(); ++i) {
try {
int output = process(data[i]);
std::cout << "処理成功: " << output << "\n";
} catch (const std::invalid_argument& e) {
std::cerr << "例外発生(index: " << i << "): " << e.what() << "\n";
}
}
return 0;
}
● 例外処理のポイント
-
process()は 負の値を見つけたら例外を投げる(throw) - 呼び出し側は tryブロックで処理し、catchで例外を受ける
- 正常系のコードがスッキリし、異常系は catch に集約される
● 実行結果
処理成功: 2
処理成功: 4
例外発生(index: 2): 負の値は処理できません(値: -3)
処理成功: 8
例外発生(index: 4): 負の値は処理できません(値: -5)
3. 分岐と例外の違いを整理する
以下は、両者の特徴を5つの観点で比較した表です。
| 観点 | 分岐(戻り値) | 例外(throw/catch) |
|---|---|---|
| 制御の明示性 | if文で明示的 | 例外が非同期的に飛ぶ |
| コードの簡潔さ | エラー判定が増え冗長 | 正常系がすっきり |
| パフォーマンス | 高速 | 例外発生時にオーバーヘッド |
| 保守性 | 分岐が増えると煩雑 | 例外型で整理しやすい |
| RAIIとの相性 | 手動で解放が必要 | スタックアンワインドで自動解放 |
● 例外処理のメリット
- 正常系のコードが読みやすくなる
- 異常系を catch に集約できる
- RAIIと組み合わせるとリソース管理が容易
● 例外処理の注意点
- 例外は「本当に例外的な事態」に使うべき
- パフォーマンスが重要な場面では避けることもある
4. まとめ:例外処理は「ロジックの本質に集中する」ための仕組み
戻り値によるエラー処理はシンプルですが、呼び出し側のコードが煩雑になりがちです。
一方、例外処理は 正常系と異常系を分離できるため、コードの見通しが良くなるという大きな利点があります。
特に今回のような「ループ中で異常値を扱う」ケースでは、例外処理を使うことで 本来の処理ロジックに集中できるようになります。