C++17 コンストラクタでの例外設計レビュー|初期化失敗と設計責務をレビューアーがどう読み解くか
この記事のポイント
- コンストラクタ内例外設計のレビュー観点を整理
- 初期化責務、契約違反、外部依存失敗をレビューアーがどう読み取るか理解できる
- リソースリーク防止と責務分界をレビューで指摘する実務技術を学ぶ
そもそも「コンストラクタでの例外設計」とは
C++ではコンストラクタ内で例外を投げる設計が許容される。
- 初期化不能(必須条件不成立)
- 外部依存取得失敗(DB接続・設定ファイル読込失敗)
- 契約違反(不正パラメータ)
一度も完成しないオブジェクトは生成自体を失敗させるべき設計である
という思想がC++の例外安全設計に組み込まれている。
注意
生成済みオブジェクトの破壊保証が無い点がC++特有の危険領域
未完全オブジェクトが存在しない設計へ収束させる責務が発生する。
なぜこれをレビューするのか
- 初期化失敗時のリソース解放責務が曖昧化しやすい
- 契約違反と外部障害が混在する実装が多い
- コンストラクタ内での副作用有無が設計混乱を生みやすい
- 破壊責務が成立しないコードをレビューで発見困難化
レビューアーは「この初期化失敗は例外通知すべきか?」を設計意図から読み解く必要がある。
レビューアー視点
- コンストラクタ内で責務を持つ初期化処理は何か?
- 契約違反は例外通知設計に統一されているか?
- 外部依存障害は捕捉設計が統一されているか?
- 副作用持ち初期化が後続処理と分離されているか?
- RAII統合設計が適切に導入されているか?
開発者視点
- 「完成しないオブジェクトは作らない」を基本思想に据える
- パラメータ契約違反は
std::invalid_argument等で例外通知 - 外部リソース失敗はカスタム例外+文脈付加設計
- 副作用はコンストラクタ外部へ明示分離を原則とする
良い実装例
パラメータ契約違反は例外化
パラメータチェック設計
#include <stdexcept>
#include <string>
class User {
public:
User(int id, const std::string& name) {
if (id <= 0) {
throw std::invalid_argument("User ID must be positive");
}
id_ = id;
name_ = name;
}
private:
int id_;
std::string name_;
};- 契約違反はコンストラクタ入口で早期失敗通知
外部依存障害はカスタム例外分離
外部障害区別
class ConfigLoadException : public std::runtime_error {
public:
explicit ConfigLoadException(const std::string& path)
: std::runtime_error("Failed to load config: " + path) {}
};
class AppConfig {
public:
AppConfig(const std::string& path) {
if (!loadConfig(path)) {
throw ConfigLoadException(path);
}
}
private:
bool loadConfig(const std::string& path) { /* ... */ return false; }
};- 外部依存失敗は業務責務名詞で区別
レビュー観点
- 完成保証のない初期化途中状態が存在しないか
- 契約違反・外部障害が責務分類整理されているか
- 副作用操作(DB登録など)が混入していないか
- リソース獲得・解放がRAII設計に統一されているか
- カスタム例外の粒度が診断容易性に貢献しているか
良くない実装例: ケース1(契約違反を握り潰し)
契約違反を無言失敗
class User {
public:
User(int id, const std::string& name) {
if (id <= 0) {
id_ = 0;
} else {
id_ = id;
}
name_ = name;
@Reviewer契約違反は例外通知に統一してください。無言でid=0代入は仕様混乱を招きます。 }
private:
int id_;
std::string name_;
};改善例
改善例(例外統一)
if (id <= 0) {
throw std::invalid_argument("User ID must be positive");
}良くない実装例: ケース2(副作用混入)
副作用混入型
class UserRepository {
public:
UserRepository() {
db_.connect();
db_.createUserTable();
@ReviewerDB副作用はコンストラクタ外部で呼び出してください。副作用有無を明示分離してください。 }
private:
Database db_;
};改善例
改善例(副作用分離)
UserRepository repo;
repo.initialize();良くない実装例: ケース3(リソースリーク可能性)
リソース解放不明確型
class Resource {
public:
Resource() {
data_ = new int[100];
if (failCondition()) {
throw std::runtime_error("Allocation failed");
}
}
~Resource() {
delete[] data_;
}
@Reviewernew/deleteはunique_ptr等RAII統合へ移行してください。例外発生時にdelete漏れが発生します。private:
int* data_;
};改善例
改善例(RAII統合)
#include <memory>
class Resource {
public:
Resource() {
data_ = std::make_unique<int[]>(100);
if (failCondition()) {
throw std::runtime_error("Allocation failed");
}
}
private:
std::unique_ptr<int[]> data_;
};PlantUML:コンストラクタ例外責務整理フロー
初期化成功保証の実務基準
| 発生対象 | 設計分類 | ハンドリング責務 |
|---|---|---|
| コンストラクタ引数不正 | 契約違反 | 開発時バグ潰し対象 |
| 外部設定取得失敗 | 外部障害 | 実行時障害監視対象 |
| 副作用操作失敗 | 設計分離対象 | コンストラクタ外部設計 |
レビューアーは「完成保証を守る設計か?」で読み取る。
ロギング統合例
生成失敗監視統合
try {
AppConfig config("config.json");
} catch (const ConfigLoadException& e) {
logger.error("Config load failure: {}", e.what());
monitoring.notify("CONFIG_FAILURE", e.what());
}- 初期化失敗は障害監視と統合運用
観点チェックリスト
まとめ
レビューアーがコンストラクタ例外設計で常に問うべきは
「完成保証を設計で守れているか?」
です。
- 完成保証 → スコープ離脱保証と統合
- 契約違反 → 例外通知統一
- 外部障害 → カスタム例外と監視統合
レビュー現場では
「コンストラクタは完成保証のみ担保させ、副作用は外部設計に分離しましょう」
という明示的レビュー文化が品質を大きく向上させます。
