この記事のポイント

  • RAIIによる例外安全性設計のレビュー観点を体系化できる
  • リソース管理責務をRAIIに収束させる設計意図をレビューで読み取れる
  • 「リソース解放漏れを起こさせない設計か?」をレビューアーが見抜く技術を整理

そもそもRAIIとは何か

RAII(Resource Acquisition Is Initialization)は
リソース獲得と解放をスコープ単位で責務一貫させる設計手法です。

  • コンストラクタでリソース獲得
  • デストラクタで解放責務
  • スコープ離脱時に自動解放保証

C++の例外安全性を最も自然に高める設計パターンとして、現場レビューではRAII採否が重要論点となります。

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

  • リソース解放漏れは最も頻繁に発生する障害
  • 例外発生時の中断経路で特に発生しやすい
  • 手動解放設計はレビューで安全性確認が困難
  • スコープ外責務分散が進むとレビューコスト爆発

RAII設計有無は「レビュー可能な設計か否か」を左右します。

レビューアー視点

  • 誰が解放責任を持つか明示されているか
  • 解放保証がスコープ単位で収束しているか
  • リソース利用順序がRAIIに委譲されているか
  • 例外耐性が自然に組み込まれているか
  • 補助RAIIクラス(スコープガード等)活用を検討しているか

開発者視点

  • 原則:リソース管理はRAII以外を極力使用しない
  • new/delete、malloc/freeをコードレビュー対象に現さない
  • ライブラリ型(unique_ptr, shared_ptr, fstreamなど)を積極採用
  • スコープ設計で責務境界を自然に整備

良い実装例

ファイル操作におけるRAII適用

fstreamによるRAII設計
#include <fstream>
#include <string>

void writeLog(const std::string& message) {
    std::ofstream ofs("log.txt");
    ofs.exceptions(std::ofstream::failbit | std::ofstream::badbit);
    ofs << message << std::endl;
}
  • コンストラクタでオープン、デストラクタで自動close
  • 例外発生時にも解放保証される

ヒープメモリのRAII化

unique_ptr活用
#include <memory>

void process() {
    std::unique_ptr<int> data(new int(42));
    *data += 1;
}
  • new/delete責務をunique_ptrが吸収
  • スコープ離脱時自動解放

複数リソースをRAIIで連鎖管理

複合RAII
#include <memory>
#include <fstream>

void fullProcess() {
    std::unique_ptr<int> data(new int(42));
    std::ofstream ofs("result.txt");
    ofs << *data << std::endl;
}
  • 全リソースの解放責任がスコープに収束

レビュー観点

  • 生のnew/deleteが存在していないか
  • delete忘れレビューが必要な設計を排除しているか
  • スコープベースで責務境界を持たせているか
  • 例外発生パスでも必ず解放が保証される設計か
  • 汎用RAIIクラス活用を怠っていないか

良くない実装例: ケース1(手動解放)

手動delete漏れ危険型
void process() {
    int* data = new int(42);
    if (*data > 0) {
        throw std::runtime_error("failure");
    }
    delete data;
@Reviewer
new/delete管理はRAII型(unique_ptr等)に委譲してください。例外発生時にdelete漏れます。
}

改善例

改善例(RAII適用)
void process() {
    std::unique_ptr<int> data(new int(42));
    if (*data > 0) {
        throw std::runtime_error("failure");
    }
}

良くない実装例: ケース2(複数解放責任の分散)

複数リソースの手動管理
void process() {
    FILE* fp = fopen("log.txt", "w");
    char* buffer = new char[1024];
    // 略
    delete[] buffer;
    fclose(fp);
@Reviewer
FILE, bufferともにRAIIクラスで管理統一してください。責務が分散し例外発生時に解放漏れリスクが上がります。
}

改善例

改善例(RAII統合管理)
#include <memory>
#include <cstdio>

struct FileCloser {
    void operator()(FILE* fp) const { if (fp) fclose(fp); }
};

void process() {
    std::unique_ptr<FILE, FileCloser> fp(fopen("log.txt", "w"));
    std::unique_ptr<char[]> buffer(new char[1024]);
    // 略
}

良くない実装例: ケース3(例外安全性放棄)

例外耐性なし
void writeFile() {
    FILE* fp = fopen("log.txt", "w");
    fprintf(fp, "start\n");
    // 途中で例外
    throw std::runtime_error("failure");
    fprintf(fp, "end\n");
    fclose(fp);
@Reviewer
例外発生時にfcloseが実行されません。スコープ離脱保証のためRAII型に統一してください。
}

改善例

改善例(RAII再設計)
void writeFile() {
    std::unique_ptr<FILE, FileCloser> fp(fopen("log.txt", "w"));
    fprintf(fp.get(), "start\n");
    throw std::runtime_error("failure");
    fprintf(fp.get(), "end\n");
}

PlantUML:RAIIによる責務吸収フロー

UML Diagram

補助ツール:スコープガードによるRAII汎用化

スコープガード型RAII
#include <functional>

class ScopeGuard {
public:
    explicit ScopeGuard(std::function<void()> f) : f_(std::move(f)), active_(true) {}
    ~ScopeGuard() { if (active_) f_(); }
    void dismiss() { active_ = false; }
private:
    std::function<void()> f_;
    bool active_;
};

void process() {
    FILE* fp = fopen("log.txt", "w");
    ScopeGuard guard([&]() { fclose(fp); });
    fprintf(fp, "start\n");
    throw std::runtime_error("failure");
}
  • C++17以前でも汎用RAIIが可能
  • C++20以降なら標準ライブラリRAII化が更に進む

観点チェックリスト

まとめ

レビューアーがRAII設計で常に問うべきは
「このスコープ離脱時に漏れが残らないか?」
です。

  • スコープベース責務収束
  • 標準ライブラリ徹底活用
  • 例外耐性自然吸収

RAII採否はレビューアーの腕の見せ所。
レビュー現場では「このリソース、RAII統一できませんか?」と
具体設計提案型のレビュー指摘が品質を決める領域です。