この記事のポイント

  • 例外時のリソース管理設計をレビューアーが読み解く技術を整理
  • 例外安全性を担保する責務分離とRAII統合のレビュー観点を解説
  • try-catchに頼らず構造で保証する設計力をレビューアーが養う

そもそも例外時のリソース管理設計とは

C++は例外が発生してもスコープ終了時にスタック巻き戻しによりデストラクタを自動呼び出します。
これを利用してリソース解放を構造保証するのがRAII(Resource Acquisition Is Initialization)設計です。

void process() {
    std::unique_ptr<ApiRequestLog> log = std::make_unique<ApiRequestLog>();
    // 例外発生してもlogは自動破棄される
}
例外安全設計の基本三層
  • 基本保証:不変式は壊れない
  • 強い保証:例外時でも状態は巻き戻る
  • 無例外保証:そもそも例外を出さない(例:swap系)

レビューアーは
「そもそも例外発生時にリソースは自動解放構造になっているか?」
を読み取ります。

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

例外発生時に以下の設計事故が起きやすいです。

  • 解放忘れ → リソースリーク
  • 所有権分散 → 解放責任不明
  • catch節に解放処理集中 → 構造肥大化
  • try-catchの乱用設計

レビュー段階で
try-catch不要でも安全設計になっているか?
を静的に評価するのがレビュー品質を決定します。

レビューアー視点

  • スマートポインタ・標準コンテナでRAII設計に寄せられているか
  • 所有権移譲が構造で表現できているか
  • try-catchはロジック回復用途だけに限定されているか
  • 手動解放ロジックが分散していないか
  • コンストラクタ例外安全性が考慮されているか

開発者視点

  • まずRAIIで全吸収を目指す
  • 手動解放責務を設計から除去
  • unique_ptr中心で例外耐性高い構造へ誘導
  • catchは原則ログ+異常報告用途に限定する

良い実装例

想定:ApiRequestLogの所有権集中設計

良い設計例
#include <memory>
#include <vector>
#include <string>
#include <iostream>

struct ApiRequestLog {
    int requestId;
    std::string endpoint;
    std::string clientIp;
    int responseCode;
    time_t requestedAt;
};

class LogManager {
public:
    void save(std::unique_ptr<ApiRequestLog> logEntry) {
        logs_.push_back(std::move(logEntry));
    }

    void dump() const {
        for (const auto& entry : logs_) {
            std::cout << entry->requestId << std::endl;
        }
    }

private:
    std::vector<std::unique_ptr<ApiRequestLog>> logs_;
};

void process() {
    auto log = std::make_unique<ApiRequestLog>();
    log->requestId = 123;

    LogManager manager;
    manager.save(std::move(log));
}

良いポイント

  • 例外発生してもlog変数は自動解放
  • save側もvector+unique_ptr統合で寿命集中
  • try-catch不要で完全構造安全

レビュー観点

  • try-catch依存せず構造自動解放になっているか
  • スマートポインタ活用で寿命責任が集中しているか
  • 所有権移譲が明示moveで表現されているか
  • delete露出がゼロか
  • コンテナ統合により複数オブジェクト寿命が吸収されているか
  • 例外発生経路で解放漏れリスクが消滅しているか

良くない実装例: ケース1(catch内に解放責任集中)

問題例①
void process() {
    ApiRequestLog* log = new ApiRequestLog();
    try {
        log->requestId = 123;
        // 他処理で例外発生の可能性
    } catch (...) {
        delete log;
        throw;
    }
    delete log;
}
@Reviewer
try-catchに解放責務を集中させる設計は保守性を損ねます。スマートポインタ設計に統一し、構造上で自動解放保証してください。

改善例

改善例①
auto log = std::make_unique<ApiRequestLog>();

良くない実装例: ケース2(所有権浮遊型設計)

問題例②
class LogManager {
public:
    void save(ApiRequestLog* logEntry) {
        logs_.push_back(logEntry);
    }

private:
    std::vector<ApiRequestLog*> logs_;
};
@Reviewer
生ポインタ格納は寿命責任が外部分散します。unique_ptr移譲に統一してください。

改善例

改善例②
void save(std::unique_ptr<ApiRequestLog> logEntry) {
    logs_.push_back(std::move(logEntry));
}

良くない実装例: ケース3(コンストラクタ例外耐性崩壊)

問題例③
class Session {
public:
    Session() {
        log_ = new ApiRequestLog();
        config_ = loadConfig(); // ここで例外可能
    }
private:
    ApiRequestLog* log_;
    Config config_;
};
@Reviewer
コンストラクタ途中例外時にnew責務が残ります。unique_ptr初期化統合で完全例外安全化してください。

改善例

改善例③
class Session {
public:
    Session() : log_(std::make_unique<ApiRequestLog>()), config_(loadConfig()) {}
private:
    std::unique_ptr<ApiRequestLog> log_;
    Config config_;
};

観点チェックリスト


まとめ

例外安全レビューは
「例外時でも破棄責任が構造で保証されるかを読むレビュー」です。

レビューアーは常に

  • catch構造に依存していないか?
  • RAIIが完全に貫通しているか?
  • 所有権が浮遊しないか?

を静的に読み解き、try-catch不要でも安全な設計へ誘導するのが役割です。

UML Diagram