C++17 RAIIによる例外安全設計レビュー|リソース管理責務と例外耐性設計をレビューアーがどう読み解くか
この記事のポイント
- 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;
@Reviewernew/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);
@ReviewerFILE, 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による責務吸収フロー
補助ツール:スコープガードによる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統一できませんか?」と
具体設計提案型のレビュー指摘が品質を決める領域です。
