C++レビュー|ヌルチェックと例外設計の責務整理とレビュー観点
この記事のポイント
- ヌルチェックと例外設計をレビューアーが読む技術を整理
- 契約による責務分界としてヌル安全性を考える
- 例外処理がどこで担保されるべきかレビューで判別できる
そもそもヌルチェック・例外とは
C++ではポインタ型・API呼び出し・リソース管理において「呼び出し失敗」「不正値」が発生します。
このとき実装者は
- ヌルチェック(null検査)
- 例外スロー(throw)
という2系統の対策を講じます。
しかし、どちらを誰が責務として担うのか
これが曖昧になるとレビュー品質が低下します。
責務整理の原則
- 契約で禁止可能ならヌル禁止に寄せる
- 回復困難な異常は例外に寄せる
- 事前条件違反は呼び出し側責務
- 事後状態失敗は呼び出され側責務
レビューアーはこの整理軸でコードを読み解きます。
なぜこれをレビューするのか
ヌルチェック・例外設計はコード品質の「後付けで崩れやすい領域」です。
レビュー段階で責務線引きを読み取れなければ以下が起きます。
- 責務の転嫁:誰がヌル判定すべきかわからなくなる
- 冗長防御:全箇所にif (x == nullptr)が氾濫
- 例外使いすぎ:正常系と異常系が混ざる
- 設計意図不読性:API契約が読めなくなる
レビューアー視点
- 呼び出し側・呼び出され側の前提契約を読む
- APIの「ヌル許容か非許容か」を判別
- 例外を責務適切に限定しているか確認
- 異常系設計の再利用性・テスト可能性を評価
開発者視点
- ヌル許容APIは極力減らす
- 事前条件違反はassert/契約設計で排除
- 回復困難な障害のみ例外に委譲
- テストコードでも異常契約確認
良い実装例
想定:APIリクエストログの記録と保存
良い設計例
struct ApiRequestLog {
int requestId;
std::string endpoint;
std::string clientIp;
int responseCode;
time_t requestedAt;
};
class LogStorage {
public:
void save(const ApiRequestLog& logEntry) {
// ストレージ処理略
std::cout << "[LOG] Stored requestId: " << logEntry.requestId << std::endl;
}
};
class ApiLogger {
public:
ApiLogger(LogStorage* storage)
: storage_(storage)
{
if (storage_ == nullptr) {
throw std::invalid_argument("storage cannot be null");
}
}
void log(const ApiRequestLog& logEntry) {
storage_->save(logEntry);
}
private:
LogStorage* storage_;
};
良いポイント
- コンストラクタ時にnull判定→契約を明示
- メソッド内では非ヌル前提でコードが簡潔
- 呼び出し側の責務範囲が明確
レビュー観点
- APIのnull許容設計が明示的か
- コンストラクタ契約で初期不正値を排除しているか
- 例外スローの範囲が限定的か
- 実行時チェックと静的契約が両立しているか
- 異常ケースのテスト容易性が高いか
良くない実装例: ケース1(呼び出し毎に冗長ヌル判定)
問題例①
class ApiLogger {
public:
ApiLogger(LogStorage* storage) : storage_(storage) {}
void log(const ApiRequestLog& logEntry) {
if (storage_ == nullptr) {
std::cerr << "Storage unavailable" << std::endl;
return;
}
storage_->save(logEntry);
}
private:
LogStorage* storage_;
};
@Reviewerコンストラクタ引数でnull禁止契約に統一してください。log()呼び出し毎の冗長なnull判定を排除し、責務を初期化時に集約しましょう。
問題点
- 呼び出し毎に同じ防御コード
- 契約責務が初期化時に集中しない
- 実装ノイズが増える
改善例
改善例①
ApiLogger(LogStorage* storage)
: storage_(storage)
{
if (storage_ == nullptr) {
throw std::invalid_argument("storage cannot be null");
}
}
良くない実装例: ケース2(異常設計の例外多発)
問題例②
class ApiLogger {
public:
void log(const ApiRequestLog* logEntry) {
if (logEntry == nullptr) {
throw std::invalid_argument("logEntry is null");
}
std::cout << logEntry->requestId << std::endl;
}
};
@Reviewerlog()呼び出し側の契約でnull禁止とし、引数をconst参照で受け取る設計に統一してください。例外多発を避け、契約側でヌル不許容を表現しましょう。
問題点
- null許容API設計になっている
- 実行時まで設計意図が不明
- テストコードの異常系確認が煩雑
改善例
改善例②
void log(const ApiRequestLog& logEntry) {
std::cout << logEntry.requestId << std::endl;
}
良くない実装例: ケース3(shared_ptrのnull過信)
問題例③
class ApiLogger {
public:
void log(std::shared_ptr<ApiRequestLog> logEntry) {
if (!logEntry) {
std::cout << "Empty log entry" << std::endl;
return;
}
std::cout << logEntry->requestId << std::endl;
}
};
@Reviewer読み取り用途でshared_ptrは不要です。所有権移譲を伴わない場合はconst参照で受け取り、null許容自体を設計から排除してください。
問題点
- 所有権不要なのにshared_ptr利用
- null許容APIに設計が流されている
- 設計責務があいまい化
改善例
改善例③
void log(const ApiRequestLog& logEntry) {
std::cout << logEntry.requestId << std::endl;
}
観点チェックリスト
まとめ
ヌルチェックと例外処理は契約設計レビューの重要ポイントです。
レビューアーは
- そもそもヌルが許されている契約か?
- 例外で投げるべき異常か?
- 初期化責任は誰が持つか?
を静的に読み取りながら、設計責務の線引きを支援することが求められます。