この記事のポイント

  • C++例外階層設計のレビュー観点を体系的に整理できる
  • カスタム例外設計の粒度・責務・分類を統合的にレビューできる
  • catch網羅性と設計責務を両立させるレビュー指摘例を実務的に習得できる

そもそも例外階層設計とは何か

C++では例外通知の基底クラスがstd::exceptionですが、実務システムではこれを業務責務ごとに分類整理した階層構造が必要になります。

std::exception
 ├── std::logic_error
 │     ├── std::invalid_argument
 │     └── ...
 └── std::runtime_error
       ├── std::overflow_error
       └── ...
  • 標準系 → 汎用抽象的分類
  • カスタム系 → ドメイン粒度での名詞化

レビューでは、階層が「分類基準として一貫しているか?」を設計意図から読み取ることが重要です。

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

  • 階層が無秩序だとcatch側の網羅性が破綻する
  • 意味粒度の統一感が失われ、保守不能になる
  • ラップ戦略や伝播戦略が曖昧化する
  • ログ・監視・障害診断が機能しなくなる

レビューアー視点

  • トップ層が整理されているか(ドメイン基底分類)
  • 論理系/実行系の明確な分離がされているか
  • 横断的分類責務(通信系/外部依存系/整合性違反系など)が整理されているか
  • ラップ設計ポリシーが定義されているか
  • catch側設計と連動する構造になっているか

開発者視点

  • 原則:標準例外系 → カスタム系の多層構造を維持
  • 粒度はドメイン名詞化レベルで具体化
  • 上位分類名を安易に抽象的にしすぎない
  • 汎用catch不能な型は極力作らない

良い例外階層設計例

全体像イメージ

std::exception
 ├── ApplicationException (プロジェクト共通基底)
 │     ├── LogicException (契約違反系)
 │     │     ├── DuplicateUserException
 │     │     └── InvalidUserIdException
 │     └── RuntimeException (実行時障害系)
 │           ├── DatabaseConnectionException
 │           └── PaymentGatewayConnectionException

具体コード例

共通基底
#include <stdexcept>
#include <string>

class ApplicationException : public std::exception {
public:
    explicit ApplicationException(const std::string& message)
        : message_(message) {}
    const char* what() const noexcept override {
        return message_.c_str();
    }
private:
    std::string message_;
};
契約違反系派生
class LogicException : public ApplicationException {
public:
    explicit LogicException(const std::string& message)
        : ApplicationException(message) {}
};

class DuplicateUserException : public LogicException {
public:
    explicit DuplicateUserException(int userId)
        : LogicException("User ID already exists: " + std::to_string(userId)) {}
};

class InvalidUserIdException : public LogicException {
public:
    explicit InvalidUserIdException(int userId)
        : LogicException("Invalid user ID: " + std::to_string(userId)) {}
};
実行時障害系派生
class RuntimeException : public ApplicationException {
public:
    explicit RuntimeException(const std::string& message)
        : ApplicationException(message) {}
};

class DatabaseConnectionException : public RuntimeException {
public:
    explicit DatabaseConnectionException(const std::string& dbName)
        : RuntimeException("DB connection failed: " + dbName) {}
};

class PaymentGatewayConnectionException : public RuntimeException {
public:
    explicit PaymentGatewayConnectionException(const std::string& gateway)
        : RuntimeException("Payment gateway unreachable: " + gateway) {}
};

レビュー観点

  • 標準例外型は基底に含まれているか
  • 契約違反系はlogic系、実行系はruntime系へ分類されているか
  • ドメイン粒度で名詞化されているか
  • what()の情報は具体性を持っているか
  • 汎用catch網羅性が維持できる構造になっているか

良くない実装例: ケース1(分類曖昧な直列乱立)

汎用名だけの乱立型
#include <string>

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

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

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

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

%% @Reviewer ドメイン粒度と分類基準が失われています。抽象汎用型の乱立はcatch網羅性を損ないます。ApplicationException基底から論理系/実行系に再整理してください。

問題点

  • 意味分類が存在せず粒度が無秩序
  • 汎用catch時に判定不能

改善例

改善例(責務階層整理)
class UserConflictException : public LogicException { ... }
class DatabaseConnectionException : public RuntimeException { ... }

良くない実装例: ケース2(catch破綻型)

catch網羅性を崩壊させる例
#include <string>

class MyCustomError {
public:
    explicit MyCustomError(const std::string& msg) : msg_(msg) {}
    std::string what() const { return msg_; }
private:
    std::string msg_;
};

void process() {
    throw MyCustomError("fatal error");
}

void run() {
    try {
        process();
    } catch (const std::exception& e) {
        // ここでは捕まらない
    }
@Reviewer
標準例外階層外の独立型は汎用catch不能です。必ずstd::exception継承構造に統一してください。
}

問題点

  • catch(std::exception&)が意味を持たなくなる
  • catch統一戦略破綻

改善例

改善例(統一継承構造適用)
class FatalApplicationError : public ApplicationException {
public:
    explicit FatalApplicationError(const std::string& message)
        : ApplicationException(message) {}
};

PlantUML:正しい階層モデル図

UML Diagram

catch網羅性設計例

網羅性を担保するcatch構造
try {
    service.run();
} catch (const LogicException& e) {
    handleLogicViolation(e);
} catch (const RuntimeException& e) {
    handleRuntimeFailure(e);
} catch (const ApplicationException& e) {
    handleUnknownDomainError(e);
} catch (const std::exception& e) {
    handleSystemError(e);
}
  • 各層の分類設計がcatch構造に対応可能
  • catch責務が可読・保守しやすくなる

観点チェックリスト

まとめ

例外階層設計レビューの本質は
「分類基準の明快さ」と「catch網羅性の保守性」
のバランスを読み取る技術です。

  • 汎用catch不能型 → 危険
  • 汎用抽象名詞乱立 → 危険
  • 業務名詞化+分類基準明示 → 安定

レビューアーは「その階層構造でcatchを書きやすいか?」を常に読み取り、
設計思想が一貫するように強く指摘していくべき領域です。