この記事のポイント

  • 例外と戻り値の使い分け設計方針をレビューアーが見極める視点を整理
  • 責務境界の読み取りと設計意図をレビューで深掘る実務的指摘例を提示
  • ケース別に「レビューでどう指摘すべきか」を具体例で習得できる

そもそも例外と戻り値エラーの違いとは

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:使い分け責務シーケンス

UML Diagram

観点チェックリスト

まとめ

レビューで常に問うべきは
「この失敗は正常フローの中に含まれるのか?」
という設計責務の分界線です。

  • 想定内は戻り値
  • 契約違反は例外

この基準をレビュー現場で浸透させることで、属人化せず読みやすい設計に統一できます。

レビューアーは常に責務境界を読み取る力を持ち、開発者が迷わぬよう明確に指摘を行う必要があります。