Top ›
C++ 入門 › STEP 11 › 第65回 noexcept と例外安全性
第65回 noexcept と例外安全性
noexcept は「この関数は例外を投げません」というコンパイラ向けの宣言です。実は vector の push_back 性能 まで左右します。併せて例外安全性の 3 レベル(basic / strong / nothrow)を押さえて、「例外があっても壊れないコード」を書けるようになりましょう。
最低限ここだけ
noexcept = 投げません宣言
破ると std::terminate
デストラクタと move は noexcept に
余裕があれば
vector の伸長が move を選ぶ条件
basic / strong / nothrow 保証
copy-and-swap イディオム
このページの流れ
1. まず触ってみる — noexcept って何の宣言?
2. vector が noexcept を気にする理由
3. 例外安全の 3 レベル
4. noexcept を付ける・付けないの判断表
5. C の経験と結びつける
6. 理解度チェック
1. まず触ってみる — noexcept って何の宣言?
noexcept は関数のシグネチャに書く約束事 です。「この関数は例外を投げません」とコンパイラに宣言します。約束を破ると std::terminate で強制終了 です(catch もできない)。
1-1. イメージ — ドアに貼る「静かにしてください」の札
noexcept は関数に貼る約束札 です。札を貼った関数は「中で例外が飛び出すようなことは絶対しません」と周囲に伝えるもの。周りのコード(特に vector のようなライブラリ)はこの札を見て最適化の選択 をします。
1-2. 最小コード
noexcept_basic.cpp C++
int add (int a, int b) noexcept { // ← 札
return a + b; // 例外投げない
}
int divide (int a, int b) { // 札なし
if (b == 0 ) throw std::runtime_error("zero" );
return a / b;
}
// コンパイル時にも使える:
static_assert (noexcept (add (1 ,2 ))); // OK
static_assert (!noexcept (divide (1 ,2 ))); // OK(投げうる)
関数の後ろに noexcept を付けるだけ。条件付き noexcept(noexcept(条件))もありますが、まずは付けるか付けないか の 2 択から入ればいいです。
1-3. 約束を破ったらどうなる?
break.cpp NG
void liar () noexcept {
throw std::runtime_error("oops" ); // 嘘つき!
}
int main () {
try { liar (); }
catch (...) { } // ← ここに届かない
} // std::terminate で即死
catch できずにプログラムが落ちます。コンパイラは警告するがエラーにしない ので、書く側が責任を持ちます。
最優先で noexcept を付けるべき関数 : デストラクタ、move コンストラクタ、move 代入、swap。理由は次節で。
2. vector が noexcept を気にする理由
vector の push_back で容量が足りなくなると、もっと大きい領域を取って既存要素を新領域へ移す 必要があります(第 3 回のアニメで見ました)。このとき vector は次の分岐をします:
move コンストラクタが noexcept → move する(速い・省メモリ)
そうでない → copy する(安全・でも遅い)
なぜか?途中で例外が飛ぶと、新領域が「半分だけ移動済み」になってしまい復旧できないから です。例外を投げない保証があれば move、そうでなければ安全な copy、と vector が自動で選んでいます。
▶ vector 伸長: move か copy か
要素型の move が noexcept かどうかを切り替えて、vector の再配置がどう変わるか見てみましょう。
move noexcept
move 投げうる(copy にフォールバック)
リセット
vector の伸長動作がここに表示されます。
move_noexcept.cpp C++
struct T {
std::string s;
T (T &&) noexcept = default ; // ★ noexcept move
T & operator =(T &&) noexcept = default ;
T (const T &) = default ;
};
static_assert (std::is_nothrow_move_constructible_v<T >);
std::vector<T > v;
v.reserve (4 );
for (int i=0 ; i<10 ; ++i) v.push_back ({...});
// 伸長時 move が使われる(noexcept 宣言のおかげ)
コンパイラ既定の move は「メンバ全員の move が noexcept なら noexcept」。 だから自作クラスは普通 = default で済みます。問題は自分で move を書いた時 — 明示的に noexcept を付けないと vector が copy を選び、意図せず遅くなります。
3. 例外安全の 3 レベル
「例外が起きても壊れない」にもグラデーションがあります。Abrahams が示した3 レベル が業界標準です。
LEVEL 1
基本保証(basic)
例外後もオブジェクトは何らかの有効な状態 。リーク無し、クラッシュ無し。ただし値は変わっているかも。最低限これは必須 。
LEVEL 2
強い保証(strong)
例外が起きたら操作前の状態に完全復帰 (ロールバック)。vector::push_back がこれ。 実装は copy-and-swap など。
LEVEL 3
例外を投げない(nothrow)
例外を絶対に投げない 。これが noexcept。dtor / move / swap は必ずここ。 最強だが制約も最強。
3-1. copy-and-swap で strong を作る定番
copy_swap.cpp C++
class Buf {
int * p_; size_t n_;
public :
Buf & operator =(Buf rhs) noexcept { // ★ 値渡し
swap (rhs); // ★ swap は投げない
return *this ;
} // rhs は dtor で旧値を解放
void swap (Buf & o) noexcept {
std::swap(p_, o.p_); std::swap(n_, o.n_);
}
};
// 利点: 途中で throw すると rhs の ctor 段階で失敗 → this は無傷
// swap は noexcept なので commit は失敗しない
ポイントは 2 つ:「失敗する可能性のある作業(複製)は commit より前に済ませる 」「commit(swap)は noexcept 」。これで操作が成功か、何もしなかったか、のどちらか になります。
4. noexcept を付ける・付けないの判断表
種類 noexcept? 理由
デストラクタ 必須 スタック巻き戻し中に投げると二重例外 = terminate
move コンストラクタ ほぼ必須 vector 伸長で move が選ばれる条件
move 代入 ほぼ必須 上に同じ
swap 必須 copy-and-swap の commit 段で投げないため
数値計算のユーティリティ 付けてよい 例外境界を越えないなら最適化に効く
コンストラクタ全般 付けない方がよい メンバの割り当て失敗で bad_alloc 等を投げうる
I/O 系 / ネットワーク 付けない 失敗が普通に起きる領域
ユーザコールバックを呼ぶ関数 付けない ユーザ側が投げるかも
4-1. 条件付き noexcept(テンプレでよく使う)
cond_noexcept.cpp C++
template <class T>
void swap (T& a, T& b) noexcept (
std::is_nothrow_move_constructible_v<T> &&
std::is_nothrow_move_assignable_v<T>
) {
T tmp = std::move (a);
a = std::move (b);
b = std::move (tmp);
}
// T の move が noexcept なら swap も noexcept
// → 依存先の契約を透過的に引き継げる
5. C の経験と結びつける
C の free() は絶対に失敗しない 契約 → C++ のデストラクタの noexcept と同じ思想。
C で「エラー時もリソースを漏らさない」ための goto cleanup; パターン → C++ では RAII + 例外 でデストラクタが自動で掃除。
C の memcpy などは失敗しない関数として設計されている → C++ の noexcept な関数は、そういう「失敗しない側」のインターフェース宣言。
コンパイラは noexcept な関数呼び出しで例外ハンドラ用テーブル を省略でき、コードサイズと実行速度が若干良くなります。
まとめ : noexcept は単なる最適化ヒントではなく、「vector などのライブラリが move を選ぶか copy を選ぶか」の契約書 。デストラクタ / move / swap は確実に付けよう。
6. 理解度チェック
4 問。
Q1. noexcept 関数が実際に例外を投げるとどうなる?
コンパイルエラーになる
呼び出し元の catch で普通に受けられる
std::terminate が呼ばれて強制終了する
noexcept は約束。破ると catch する暇もなく terminate です。コンパイラは警告程度で、エラーにはしません。
Q2. vector<T> が push_back で伸長する時、T の move が noexcept でないとどうなる?
安全のため copy にフォールバックする
強制的に move が使われる
コンパイルエラーになる
vector は strong guarantee を保つため、move が投げうる場合は copy を選ぶ。自作型は move を noexcept に宣言するのが鉄則。
Q3. 「strong guarantee」の定義として正しいのは?
例外を絶対に投げない
例外が起きたら操作前の状態に完全復帰する
例外が起きても有効な状態を保つ(値は変わりうる)
strong = ロールバック保証。「何もしなかったか、全部成功したか」のどちらかになる。copy-and-swap で実現するのが定番。
Q4. 次のうち noexcept を「付けないほうがいい」のは?
デストラクタ
swap
std::string を受け取ってファイルを開くコンストラクタ
ファイル I/O は普通に失敗する(存在しない、権限ない)。そこで noexcept を付けると、失敗のたびに terminate してしまう。