C++17例外安全性レビュー|基本保証・強い保証の設計とレビュー指摘実例集
この記事のポイント
- 例外安全性の「保証レベル」をレビューでどう読み取るか理解できる
- 開発者が迷わないレビュー指摘の具体例を習得できる
- 基本保証・強い保証の違いを実装例ベースで読み解く
そもそも例外安全性とは
C++は例外をサポートしていますが、例外が発生してもプログラムが安全に動作を続けられる設計が必要です。この考え方を例外安全性(exception safety)と呼びます。
例外安全性は、例外が発生した時にオブジェクトやシステム全体がどのような状態に保たれるかを定義する考え方です。代表的な保証レベルは以下の通りです。
| 保証レベル | 説明 |
|---|---|
| no-throw保証 | 絶対に例外を投げない |
| 強い保証 | 例外発生時でも、呼び出し前の状態を維持 |
| 基本保証 | 例外発生時でも、破壊的でない有効状態を維持(ただし内容は未定) |
本記事は実務レビューで頻出する基本保証と強い保証を中心に扱います。
なぜこれをレビューするのか
例外安全性はレビューアーが積極的に確認すべき重要領域です。理由は以下に整理できます。
- 例外が発生した場合の影響を事前に把握する必要があるため
- データ破壊やリソースリークを未然に防ぐため
- API設計時に利用側の契約を明確にするため
- 例外ハンドリングが属人的・場当たり的にならないよう整備するため
レビューアー視点
レビューアーは以下を重点確認します。
- どの処理が例外を投げる可能性があるか
- 例外発生時にオブジェクトが一貫性を保てるか
- ロールバック設計の有無
- noexcept指定の妥当性
- スコープベースのリソース管理(RAII)設計
開発者視点
開発者は以下を考慮して設計します。
- 必要な保証レベルを明示的に選択
- 例外発生時のシナリオを事前整理
- 例外が発生する可能性のあるAPI呼び出しを把握
- 状態復元の手段を検討(コピー・ムーブ・トランザクション等)
良い実装例
まず、強い保証を提供する実装例を示します。
強い保証の設計例
#include <vector>
#include <string>
class RequestQueue {
public:
void addRequest(const std::string& request) {
std::vector<std::string> temp = requests_;
temp.push_back(request);
requests_ = std::move(temp);
}
private:
std::vector<std::string> requests_;
};実装ポイント
- 一時オブジェクト
tempに変更を適用 - 本体
requests_には例外なしでのみ更新 - ロールバック処理を暗黙的に実現
これにより「強い保証(strong guarantee)」が提供されています。
レビュー観点
レビュー時に確認すべき観点は以下です。
- 例外発生があり得るAPI呼び出し箇所の特定(new, STL操作など)
- 操作順序に副作用の逆流が起こり得ない設計か
- ロールバック可能な構造か(コピー・ムーブ戦略の適切さ)
- スコープで閉じたRAII設計になっているか
- noexcept指定が過剰/不足していないか
良くない実装例: ケース1(基本保証止まり)
基本保証のみの実装例
#include <vector>
#include <string>
class RequestQueue {
public:
void addRequest(const std::string& request) {
requests_.push_back(request);
@Reviewerpush_backの前にrequests_.reserve(requests_.size() + 1)を事前に呼び出してください。内部メモリ再確保が例外を投げた場合、requests_が部分的に変更された状態になります。事前確保で例外発生ポイントを事前に分離し、強い保証に近づけましょう。 }
private:
std::vector<std::string> requests_;
};問題点
push_back内部の再確保で例外が発生可能- 成功時と失敗時で部分的に状態が変更されるリスク
- 呼び出し側が整合性を保証できなくなる
改善例
改善例(強い保証に昇格)
#include <vector>
#include <string>
class RequestQueue {
public:
void addRequest(const std::string& request) {
std::vector<std::string> temp = requests_;
temp.push_back(request);
requests_ = std::move(temp);
}
private:
std::vector<std::string> requests_;
};改善理由
- 変更適用前に一時オブジェクトへ安全に適用
- 例外発生後も本体は旧状態を維持
良くない実装例: ケース2(リソース管理不足)
リソースリークの可能性例
#include <fstream>
#include <string>
class ApiRequestLogger {
public:
void log(const std::string& endpoint) {
std::ofstream ofs("log.txt");
ofs << endpoint << std::endl;
@Reviewerofstreamのコンストラクタopenに失敗する可能性があります。先にofs.open()とexceptions()を組み合わせて、例外伝播方針を明示してください。またリソース確保と利用は分離してください。 }
};問題点
ofstreamのコンストラクタが例外を投げ得る- 出力失敗時にファイルが中途半端に作成される可能性
改善例
改善例(RAII適用・例外設計強化)
#include <fstream>
#include <string>
class ApiRequestLogger {
public:
void log(const std::string& endpoint) {
std::ofstream ofs;
ofs.exceptions(std::ofstream::failbit | std::ofstream::badbit);
ofs.open("log.txt");
ofs << endpoint << std::endl;
}
};改善理由
- 明示的にopen段階で例外を発生させるポリシー指定
- 出力失敗も含めて一貫して例外で通知
- closeは自動実行(RAII適用)
PlantUMLによるフロー整理
観点チェックリスト
まとめ
例外安全性は保証レベルの明示→構造の具体化→副作用排除という順でレビューが進みます。レビューアーは、常に「例外がここで出たら、このオブジェクトは有効状態か?」をシミュレーションする習慣を持つことが重要です。
レビューで迷わせない指摘とは「理由+具体手段の提示」です。今回紹介したようにreserve()を呼び出すように修正してください、RAII設計を適用してくださいと行動指示まで含めるのがプロのレビュー指摘です。
