C++17例外と戻り値エラーの使い分けレビュー|責務境界と設計意図を読み解く実務指摘集
この記事のポイント
- 例外と戻り値の使い分け設計方針をレビューアーが見極める視点を整理
- 責務境界の読み取りと設計意図をレビューで深掘る実務的指摘例を提示
- ケース別に「レビューでどう指摘すべきか」を具体例で習得できる
そもそも例外と戻り値エラーの違いとは
C++17ではエラーハンドリングを2系統から選択できます。
| 区分 | 用途 | 概要 |
|---|---|---|
例外 (throw) |
契約違反・想定外障害 | 実行不能、設計違反、重大破綻を通知 |
戻り値 (return) |
想定内の失敗 | 入力不足・条件未成立・選択肢分岐 |
レビューアーは「エラーの種類が設計責務に対して妥当か?」を常に確認する必要があります。
なぜこれをレビューするのか
レビューを行う理由は以下に整理できます。
- 契約違反と想定内失敗が混在してバグ温床になる
- 例外/戻り値の濫用で読解不能なコードに陥る
- 呼び出し元のハンドリング責任が適切に配置されていない
- プロジェクト内で方針統一されないまま属人化が進む
レビューアー視点
レビューアーは以下を着眼点に読み取ります。
- 想定内失敗か?契約違反か?(責務境界)
- 誰がハンドリング責任を持つのか?(契約分離)
- 副作用が発生前か?発生後か?(整合性保証)
- 失敗頻度・コストを考慮しているか?(パフォーマンス配慮)
- 例外階層の設計が定義されているか?(設計整理)
開発者視点
設計時に考慮する基準は以下です。
- ユーザ入力系は戻り値型で通知
- 実装契約違反は例外型で通知
- 正常系は極力if/return構造内で完結
- 異常系は例外伝播で明示的に通知
良い実装例
想定内失敗:戻り値使用
戻り値利用の適切例
#include <optional>
#include <string>
#include <map>
class UserRepository {
public:
std::optional<std::string> getUserName(int userId) const {
auto it = userTable_.find(userId);
if (it == userTable_.end()) {
return std::nullopt;
}
return it->second;
}
private:
std::map<int, std::string> userTable_;
};optionalにより「存在しない可能性」を型契約で表現- 呼び出し側が正常フローの中で分岐できる設計
契約違反:例外使用
契約違反は例外で通知
#include <stdexcept>
#include <string>
class ApiRequestValidator {
public:
void validateRequestId(int requestId) {
if (requestId <= 0) {
throw std::invalid_argument("Request ID must be positive");
}
}
};- 負値IDは設計上の契約違反 → 例外による中断通知
レビュー観点
レビューで確認すべき観点は以下です。
- 想定内失敗は戻り値設計になっているか?
- 契約違反は例外設計になっているか?
- 頻出パスで例外を過剰使用していないか?
- 副作用発生後に例外通知していないか?
- 呼び出し元に不要なif分岐を強制していないか?
- 設計基準がプロジェクト内で統一されているか?
良くない実装例: ケース1(契約違反の戻り値化)
契約違反を戻り値で隠す悪例
#include <string>
class ApiRequestValidator {
public:
bool validateRequestId(int requestId, std::string& errorMessage) {
if (requestId <= 0) {
errorMessage = "Request ID must be positive";
return false;
}
return true;
}
@Reviewer負のIDは契約違反です。bool返却ではなく、std::invalid_argumentをthrowして例外通知に変更してください。 }
};問題点
- 呼び出し契約違反(負のID)が戻り値で曖昧化
- 呼び出し元がif分岐だらけになり設計が肥大化
改善例
改善例(契約違反は例外)
#include <stdexcept>
#include <string>
class ApiRequestValidator {
public:
void validateRequestId(int requestId) {
if (requestId <= 0) {
throw std::invalid_argument("Request ID must be positive");
}
}
};良くない実装例: ケース2(想定内失敗を例外化)
想定内失敗を例外化している悪例
#include <stdexcept>
#include <string>
#include <map>
class UserRepository {
public:
std::string getUserName(int userId) const {
auto it = userTable_.find(userId);
if (it == userTable_.end()) {
throw std::runtime_error("User not found");
}
return it->second;
}
@ReviewerユーザID未登録は想定内失敗です。例外ではなくstd::optionalを使用し戻り値型で存在有無を返してください。 }
private:
std::map<int, std::string> userTable_;
};問題点
- 想定内条件で例外発生 → 呼び出し元が例外ハンドラ濫用
改善例
改善例(想定内失敗は戻り値)
#include <optional>
#include <string>
#include <map>
class UserRepository {
public:
std::optional<std::string> getUserName(int userId) const {
auto it = userTable_.find(userId);
if (it == userTable_.end()) {
return std::nullopt;
}
return it->second;
}
private:
std::map<int, std::string> userTable_;
};PlantUML:使い分け責務シーケンス
観点チェックリスト
まとめ
レビューで常に問うべきは
「この失敗は正常フローの中に含まれるのか?」
という設計責務の分界線です。
- 想定内は戻り値
- 契約違反は例外
この基準をレビュー現場で浸透させることで、属人化せず読みやすい設計に統一できます。
レビューアーは常に責務境界を読み取る力を持ち、開発者が迷わぬよう明確に指摘を行う必要があります。
