C++17例外漏洩防止とcatch網羅性レビュー|例外流出経路とハンドリング責務の設計指摘技法
この記事のポイント
- 例外漏洩(未処理例外)のレビュー観点を整理できる
- catch網羅性と責務分界線を設計レビューで読み取る技術を学べる
- 開発者が迷わぬレビュー指摘例を実践的に理解できる
そもそも例外漏洩とは
C++における例外漏洩(Exception Leakage)とは、想定される例外が呼び出し元に伝播し続け、処理不能状態まで到達してしまう設計上の欠陥を指します。
- ハンドリングすべき責務層でキャッチされずに伝播
- システムの外側まで例外が漏れ、std::terminateが発動
- ログ出力・通知・復旧処理などが行われない
レビューアーは例外発生点だけでなく、例外がどの層で止められる設計なのかを読み取る責務を負います。
なぜこれをレビューするのか
- 例外伝播設計は責務境界と密接に結びつく
- 層ごとのハンドリング契約が未整備だと障害が表面化する
- catch網羅性が甘いと復旧不能障害や障害分析不能状態になる
- ログ・監視・通知責務の所在が不明確になる
レビューアー視点
- 例外はどの層で収束させるのかを設計として確認
- 例外階層型の整理が行われているか
- catch節で何を握り潰し何を伝播させるのか明確化されているか
- 必要に応じて上位層に情報付加(ラップ)されているか
開発者視点
- 自層の責務範囲で処理可能かを見極める
- 処理不能なら上位に文脈付加して伝播する
- 決して例外を握り潰さない
- ログ・通知・監視の発火点はcatch設計とセットで整備
良い実装例
責務分界が整理された例外伝播
#include <stdexcept>
#include <string>
#include <iostream>
class Repository {
public:
void saveData() {
throw std::runtime_error("DB connection failed");
}
};
class Service {
public:
void process() {
try {
repository_.saveData();
} catch (const std::exception& e) {
throw std::runtime_error(std::string("Service processing failed: ") + e.what());
}
}
private:
Repository repository_;
};
class Application {
public:
void run() {
try {
service_.process();
} catch (const std::exception& e) {
logError(e);
}
}
void logError(const std::exception& e) {
std::cerr << "Fatal Error: " << e.what() << std::endl;
}
private:
Service service_;
};- 層ごとに責務分界されたcatch設計
- 上位層はログ通知・監視責務を担当
- 途中で握り潰さず文脈付加で伝播
レビュー観点
- 業務責務の層単位で例外収束設計が取られているか
- 握り潰し(空catch)が存在していないか
- catch網羅性が十分か(論理網羅・型網羅)
- ログ・通知がcatch設計に統合されているか
- 例外情報が失われずラップされているか
良くない実装例: ケース1(握り潰し)
握り潰しによる障害隠蔽
#include <stdexcept>
#include <iostream>
class Repository {
public:
void saveData() {
throw std::runtime_error("DB failure");
}
};
class Service {
public:
void process() {
try {
repository_.saveData();
} catch (...) {
// do nothing
@Reviewer例外を握り潰しています。catch(...)は例外情報を破棄し、障害原因の特定不能に繋がります。最低限ログ出力または上位伝播させてください。 }
}
private:
Repository repository_;
};問題点
- catch節で障害情報が完全消失
- 監視・通知不能な設計欠陥
改善例
改善例(ログ通知統合)
#include <stdexcept>
#include <iostream>
class Service {
public:
void process() {
try {
repository_.saveData();
} catch (const std::exception& e) {
std::cerr << "Service error: " << e.what() << std::endl;
throw; // 上位へ伝播維持
}
}
private:
Repository repository_;
};良くない実装例: ケース2(catch網羅性不足)
catch範囲の狭さで漏洩
#include <stdexcept>
#include <iostream>
class Service {
public:
void process() {
try {
repository_.saveData();
} catch (const std::invalid_argument& e) {
std::cerr << "Invalid arg: " << e.what() << std::endl;
}
@Reviewercatch対象が限定されすぎています。他のstd::exception系や実行時障害も適切に網羅してください。 }
private:
Repository repository_;
};問題点
- 論理的に発生可能な例外型を網羅していない
- キャッチ漏れで上位伝播が不意に発生する
改善例
改善例(型網羅性強化)
#include <stdexcept>
#include <iostream>
class Service {
public:
void process() {
try {
repository_.saveData();
} catch (const std::invalid_argument& e) {
std::cerr << "Invalid arg: " << e.what() << std::endl;
} catch (const std::exception& e) {
std::cerr << "Other exception: " << e.what() << std::endl;
}
}
private:
Repository repository_;
};PlantUML:例外伝播とcatch網羅性フロー
観点チェックリスト
まとめ
レビューアーの最大の役割は
「ここで例外が止まるのか?それとも流れるのか?」
という例外経路設計の意図を読み取ることにあります。
例外漏洩を放置すれば、
- 障害発生時の原因特定不能
- システム異常終了
- ログ空白
という致命的障害に直結します。
レビュー現場ではcatch設計を流れ全体で可視化し、責務層を越境させない設計指摘を習慣にしていくことが重要です。
