この記事のポイント

  • ヌルチェックと例外設計をレビューアーが読む技術を整理
  • 契約による責務分界としてヌル安全性を考える
  • 例外処理がどこで担保されるべきかレビューで判別できる

そもそもヌルチェック・例外とは

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;
    }
};
@Reviewer
log()呼び出し側の契約で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;
}

観点チェックリスト


まとめ

ヌルチェックと例外処理は契約設計レビューの重要ポイントです。
レビューアーは

  • そもそもヌルが許されている契約か?
  • 例外で投げるべき異常か?
  • 初期化責任は誰が持つか?

を静的に読み取りながら、設計責務の線引きを支援することが求められます。

UML Diagram