導入
C++のstd::vectorを使っている際、要素数が増えてメモリの再確保(リサイズ)が行われる場面があるかと思います。実は、この再確保の処理において、クラスのムーブコンストラクタに「noexcept」を指定しているかどうかで、パフォーマンスに劇的な差が生まれることをご存知でしょうか。今回は、この「暗黙のパフォーマンス・ペナルティ」の仕組みと、なぜnoexceptが重要なのかを解説します。
基礎知識
std::vectorは、メモリが不足すると新しい領域を確保し、既存の要素を新しい場所へ移動させます。このとき、もし移動の途中で例外が発生したらどうなるでしょうか。コピーであれば元のデータは無事ですが、ムーブは「元のオブジェクトの状態を破壊」してしまうため、例外が発生すると元のデータが壊れたまま復元できなくなります。
これを防ぐため、STLのコンテナは「強い例外安全性」を保証する設計になっています。コンパイラは「この型のムーブは絶対に失敗しない(noexceptである)」と確証が持てない限り、安全のためにムーブではなくコピーを選択します。これがstd::move_if_noexceptの基本的な考え方です。
実装と解決策
ムーブコンストラクタやムーブ代入演算子を定義する際は、必ずnoexceptを付与してください。これにより、コンパイラはstd::is_nothrow_move_constructible_vをtrueと判定し、vectorの拡張時に高速なムーブ処理を選択するようになります。また、自分で定義しなくても良い場合は、コンパイラに自動生成させる「Rule of Zero」を意識し、可能な限り明示的な定義を避けることも有効な戦略です。
サンプルプログラム
以下のコードで、noexceptの有無による動作の違いを確認できます。
#include
include
include
// noexceptを指定したクラス
struct FastType {
FastType() = default;
FastType(FastType&&) noexcept { std::cout << "ムーブされました" << std::endl; }
};
// noexceptを指定し忘れたクラス
struct SlowType {
SlowType() = default;
SlowType(SlowType&&) { std::cout << "コピーされました" << std::endl; }
};
int main() {
std::vector
vec1.emplace_back();
// 再確保が発生するとムーブが使われる
vec1.emplace_back();
std::vector
vec2.emplace_back();
// noexceptがないため、安全のためにコピーが選ばれる
vec2.emplace_back();
return 0;
}
応用・注意点
この問題は、特に「Rule of Five」を適用して独自にムーブコンストラクタを実装する際に陥りやすい罠です。デフォルト実装では自動的にnoexceptが付与されますが、自分でムーブコンストラクタを書く際には、必ず末尾にnoexceptを書き忘れないように注意してください。
また、複雑なクラスでは「基底クラスやメンバ変数のムーブコンストラクタがnoexceptであるか」も重要になります。確信が持てない場合は、static_assert(std::is_nothrow_move_constructible_v

コメント