この記事のポイント

  • noexcept指定のレビュー観点を実務レベルで理解できる
  • 副作用と例外安全の関係を設計・レビュー両面から整理
  • 開発者が迷わないレビュー指摘例を習得できる

そもそもnoexceptとは

C++では例外が発生しうることを型シグネチャで表明することはできません。代わりに関数が例外を投げないと約束する場合にnoexceptを使用します。

void doSomething() noexcept;

noexceptは以下の3つを意味します。

  • 設計契約の明示
    「この関数は例外を投げません」という呼び出し側への約束
  • 最適化ヒント
    コンパイラに対してコード生成・分岐省略を促せる
  • 強制契約
    実際に例外が発生すればstd::terminateが呼ばれプログラムは異常終了

よって、レビューではnoexceptが付与されている箇所を「例外非発生が保証できるのか?」という契約監査の目で確認する必要があります。

なぜこれをレビューするのか

noexceptは設計契約である以上、付け忘れも乱用も障害原因になります。

レビュー対象になる主な理由

  • noexceptが必要な場面で未指定 → パフォーマンス劣化やmove最適化不能
  • noexceptが安易に付与 → 将来的な例外漏洩で予期せぬ異常終了
  • noexcept有無が副作用の制御責任を曖昧化 → 設計意図が読めなくなる

実害が発生する典型パターン

  • std::vector::emplace_back()等が内部でmoveする際、対象型のmoveがnoexceptでないと再確保アルゴリズムが退避コピー戦略を取る
  • 標準ライブラリの多くはnoexcept推論を行うため、ユーザ定義型のnoexcept指定が連鎖的に影響
  • noexcept指定忘れによりムーブセマンティクスが無駄に非効率

レビューアー視点

レビューアーは以下を確認します。

  • noexceptが型全体の設計方針として一貫しているか
  • noexcept指定関数が内部で例外を投げる操作を行っていないか
  • 副作用を持つ関数に対する例外発生時の状態保証設計が成立しているか
  • 移動コンストラクタ・スワップ等の特殊メンバ関数に正しく指定されているか

開発者視点

開発者は以下を検討します。

  • noexcept付与は消極的デフォルトとし、設計確認後に積極指定
  • noexcept違反が発生した際にどうfailするか(terminateポリシー)を整理
  • 副作用を持つ関数はロールバック設計と整合性維持戦略を整理
  • noexcept条件をnoexcept(expr)で部分式指定する習慣を持つ

良い実装例

まず、設計意図が明確なnoexcept指定例を示します。

正しいnoexcept指定例
#include <utility>
#include <string>

class ApiRequestLog {
public:
    ApiRequestLog() noexcept = default;

    ApiRequestLog(ApiRequestLog&& other) noexcept 
        : requestId_(other.requestId_),
          endpoint_(std::move(other.endpoint_)),
          clientIp_(std::move(other.clientIp_)),
          responseCode_(other.responseCode_),
          requestedAt_(other.requestedAt_) {}

    ApiRequestLog& operator=(ApiRequestLog&& other) noexcept {
        requestId_ = other.requestId_;
        endpoint_ = std::move(other.endpoint_);
        clientIp_ = std::move(other.clientIp_);
        responseCode_ = other.responseCode_;
        requestedAt_ = other.requestedAt_;
        return *this;
    }

private:
    int requestId_;
    std::string endpoint_;
    std::string clientIp_;
    int responseCode_;
    std::string requestedAt_;
};

ポイント解説

  • 移動コンストラクタ・移動代入演算子がともにnoexcept指定済み
  • メンバのムーブ操作が全てnoexcept(標準ライブラリ型はムーブがnoexcept)
  • 明示的指定により、標準ライブラリ使用時の内部move最適化を阻害しない

レビュー観点

noexceptレビューの基本チェックは以下です。

  • noexcept指定関数内に例外発生可能API呼び出しがないか
  • move/swapのnoexcept漏れはないか
  • noexcept(expr)による式評価条件化が必要ではないか
  • noexceptによるterminate強制契約が安全に管理されているか
  • noexcept付与に社内方針・一貫ルールが定まっているか

良くない実装例: ケース1(移動系のnoexcept漏れ)

noexcept漏れの例
#include <string>

class ApiRequestLog {
public:
    ApiRequestLog() = default;

    ApiRequestLog(ApiRequestLog&& other)
        : requestId_(other.requestId_),
          endpoint_(std::move(other.endpoint_)),
          clientIp_(std::move(other.clientIp_)),
          responseCode_(other.responseCode_),
          requestedAt_(other.requestedAt_) {}

private:
    int requestId_;
    std::string endpoint_;
    std::string clientIp_;
    int responseCode_;
    std::string requestedAt_;
};
@Reviewer
移動コンストラクタにnoexceptが指定されていません。ムーブ可能型の最適化に影響します。ApiRequestLog(ApiRequestLog&&) noexcept { ... } に修正してください。

問題点

  • 移動系にnoexceptが欠落している
  • STLコンテナ使用時に退避コピーへ退化

改善例

改善例(移動系のnoexcept追加)
#include <string>

class ApiRequestLog {
public:
    ApiRequestLog() noexcept = default;

    ApiRequestLog(ApiRequestLog&& other) noexcept
        : requestId_(other.requestId_),
          endpoint_(std::move(other.endpoint_)),
          clientIp_(std::move(other.clientIp_)),
          responseCode_(other.responseCode_),
          requestedAt_(std::move(other.requestedAt_)) {}

private:
    int requestId_;
    std::string endpoint_;
    std::string clientIp_;
    int responseCode_;
    std::string requestedAt_;
};

良くない実装例: ケース2(内部例外の可能性放置)

内部で例外発生可能API使用
#include <vector>
#include <string>

class RequestQueue {
public:
    void clearAndReserve(size_t n) noexcept {
        requests_.clear();
        requests_.reserve(n);
    }

private:
    std::vector<std::string> requests_;
};
@Reviewer
reserveは内部でメモリ再確保を伴いstd::bad_allocを投げ得ます。この状態でnoexceptを付与するのは契約破綻です。noexceptは外してください。

問題点

  • reserve()がnewを内部使用
  • 例外が出る可能性があるのにnoexcept指定

改善例

改善例(契約整合性の回復)
#include <vector>
#include <string>

class RequestQueue {
public:
    void clearAndReserve(size_t n) {
        requests_.clear();
        requests_.reserve(n);
    }

private:
    std::vector<std::string> requests_;
};

補足

  • 単純にnoexcept指定を外し契約破綻を回避

PlantUMLによる副作用設計と契約崩壊例

UML Diagram

観点チェックリスト

まとめ

noexceptレビューは契約管理レビューです。性能最適化や設計文脈で積極指定するのは有効ですが、指定根拠を持たずに付与するのは最も危険です。

レビューアーは「例外が出たらterminateしても安全と言えるか?」を常に意識し、開発者が迷わぬよう「外すべき」「付けるべき」「条件指定にすべき」を明示指摘することが求められます。