この記事のポイント

  • std::exception系の正しい継承設計をレビューで読み取る視点を整理
  • カスタム例外の定義指針をレビューで指摘する実務例を提示
  • 例外クラス設計責務をレビューアーが深掘る技術を解説

そもそも標準例外型継承とは

C++では例外通知の基底型として std::exception が用意されています。

標準例外体系は次のように整理できます:

std::exception
 ├── std::logic_error
 │     ├── std::invalid_argument
 │     ├── std::domain_error
 │     ├── std::length_error
 │     └── std::out_of_range
 └── std::runtime_error
       ├── std::range_error
       ├── std::overflow_error
       └── std::underflow_error
補足:2系統の意味
  • logic_error系:設計契約違反、静的検証可能な論理矛盾
  • runtime_error系:実行時障害、外部環境依存で発生

カスタム例外を定義する際はこれら標準型のどこに位置付けるべきかをレビューアーが設計意図から読み解くことが重要です。

なぜこれをレビューするのか

  • カスタム例外を乱立させやすい設計領域
  • 基底型不適切選択 → catch網羅性低下・意味不明な階層化
  • 標準例外系と互換しない型 → 汎用catch不能
  • 例外意味の粒度不統一 → 障害解析困難化

レビューアー視点

  • 例外の意味粒度・分類根拠が整理されているか
  • 標準階層に自然帰属可能か検討した上での継承判断か
  • 汎用catch用にstd::exception系継承が維持されているか
  • 例外情報の付加設計(what()の詳細化)が行われているか
  • 継承禁止(final指定)検討がされているか

開発者視点

  • 例外クラスを安易に乱立しない
  • 既存標準例外型の活用を優先
  • カスタム化はドメイン責務名詞化を重視
  • what()情報は詳細化して障害解析に貢献させる

良い実装例

標準系からの自然継承例
#include <stdexcept>
#include <string>

class InvalidUserIdException : public std::invalid_argument {
public:
    explicit InvalidUserIdException(int userId)
        : std::invalid_argument("Invalid user ID: " + std::to_string(userId)) {}
};
  • 負のIDは契約違反invalid_argument系統へ自然継承
  • what()情報が具体値を含み障害解析容易化
実行系障害はruntime_error系継承
#include <stdexcept>
#include <string>

class DatabaseConnectionException : public std::runtime_error {
public:
    explicit DatabaseConnectionException(const std::string& dbName)
        : std::runtime_error("DB connection failed: " + dbName) {}
};
  • 外部環境依存の実行時障害 → runtime_error系統へ整理

レビュー観点

  • 契約違反 vs 実行時障害の分類が適切か
  • 標準型へ帰属可能なものをカスタム乱立していないか
  • what()が十分に障害診断情報を含んでいるか
  • std::exception系継承が維持されているか
  • 継承ツリーに汎用性あるcatch網羅設計が残っているか

良くない実装例: ケース1(基底型継承忘れ)

std::exception系継承漏れ
#include <string>

class InvalidUserId {
public:
    InvalidUserId(int userId) : userId_(userId) {}

    std::string what() const {
        return "Invalid user ID: " + std::to_string(userId_);
    }

private:
    int userId_;
};

%% @Reviewer std::exception系を継承していないためcatch(std::exception&)での網羅が不能になります。必ず標準系から適切に派生してください。

問題点

  • what()は実装されているが標準系との互換性がない
  • 汎用catch不能

改善例

改善例(標準系継承追加)
#include <stdexcept>
#include <string>

class InvalidUserIdException : public std::invalid_argument {
public:
    explicit InvalidUserIdException(int userId)
        : std::invalid_argument("Invalid user ID: " + std::to_string(userId)) {}
};

良くない実装例: ケース2(分類根拠不明瞭)

カスタム例外乱立
#include <string>

class UserException {
public:
    explicit UserException(const std::string& message) : message_(message) {}

    std::string what() const { return message_; }

private:
    std::string message_;
};

%% @Reviewer 汎用UserException型は分類粒度が曖昧です。論理系はlogic_error系、実行系はruntime_error系に分離し意味的粒度を明確化してください。

問題点

  • 例外粒度が不統一
  • 呼び出し側で意図を読み取れない

改善例

改善例(粒度整理)
#include <stdexcept>
#include <string>

class UserNotFoundException : public std::runtime_error {
public:
    explicit UserNotFoundException(int userId)
        : std::runtime_error("User not found: " + std::to_string(userId)) {}
};

PlantUML:標準例外継承構造

UML Diagram

観点チェックリスト

まとめ

レビューアーが例外設計で確認すべき本質は
「その例外は何を伝えたいのか?」
という分類粒度とcatch設計の可読性です。

標準例外型に自然帰属させつつ、プロジェクト固有の意味名詞だけを上乗せする設計が現場では安定します。

レビュー現場では
「標準例外型から正しく継承しましょう」
を徹底指摘する文化を根付かせることが、
保守性・診断性・可読性を守る最大の防御線となります。