C++の標準ライブラリにおいて、std::vectorは最も頻繁に利用されるコンテナの一つです。
動的配列としての利便性を備える一方で、要素の削除操作は、実装方法によってパフォーマンスやコードの安全性に大きな差が生まれる処理でもあります。
特に、大量のデータを扱う際やループ内での削除処理では、イテレータの無効化や要素の再配置(シフト)に伴うコストを正しく理解しておく必要があります。
本記事では、従来のメンバ関数としてのeraseから、C++20で導入された便利なstd::eraseまで、状況に応じた最適な削除手法を詳しく解説します。
vector::erase メンバ関数の基本
std::vectorには、自身の要素を削除するためのメンバ関数としてeraseが用意されています。
この関数には、単一の要素を削除する形式と、特定の範囲をまとめて削除する形式の2種類があります。
単一要素の削除
特定のイテレータが指す要素を削除する場合、v.erase(it)の形式を使用します。
この操作を行うと、削除された要素よりも後ろにあるすべての要素が前方に一つずつ詰められます。
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {10, 20, 30, 40, 50};
// 3番目の要素(30)を削除
auto it = v.begin() + 2;
v.erase(it);
for (int x : v) {
std::cout << x << " ";
}
return 0;
}
10 20 40 50
このとき注意すべき点は、削除された位置以降のイテレータ、参照、ポインタがすべて無効化されるという点です。
範囲指定による削除
複数の要素を一度に削除したい場合は、開始イテレータと終了イテレータ(半開区間)を引数に取る形式を使用します。
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6};
// 2番目から4番目まで(値: 2, 3, 4)を削除
v.erase(v.begin() + 1, v.begin() + 4);
for (int x : v) {
std::cout << x << " ";
}
return 0;
}
1 5 6
この範囲削除は、単一要素の削除を繰り返すよりも圧倒的に効率的です。
なぜなら、要素の移動(シフト)が一度で済むためです。
イテレータの無効化とその対策
std::vector::eraseを使用する上で最も陥りやすいバグが、ループ内での削除です。
要素を削除すると、その位置以降のイテレータが無効になるため、単純にit++を行うと未定義動作を引き起こします。
正しいループ内削除の書き方
eraseメンバ関数は、削除された要素の「次の要素」を指す有効なイテレータを返します。
これを利用するのが正しい方法です。
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6};
for (auto it = v.begin(); it != v.end(); ) {
if (*it % 2 == 0) {
// 削除して次の有効なイテレータを受け取る
it = v.erase(it);
} else {
// 削除しない場合のみインクリメント
++it;
}
}
for (int x : v) {
std::cout << x << " ";
}
return 0;
}
このコードでは、偶数の要素のみを削除しています。
eraseを呼び出した際は戻り値をitに代入し、そうでない場合のみ++itを実行することで、要素をスキップしたり無効なイテレータを参照したりするリスクを回避できます。
効率的な条件削除:Erase-Removeイディオム
C++20より前のバージョンにおいて、特定の条件を満たす要素をすべて削除する標準的な手法が「Erase-Removeイディオム」です。
std::remove の仕組み
std::remove(<algorithm>ヘッダ)は、名前に反して「実際に要素を削除する」わけではありません。
条件に合致しない要素を前方に詰め、不要になった要素をコンテナの末尾に残すという動作をします。
戻り値として、有効な要素の末尾の次を指すイテレータを返します。
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> v = {1, 2, 3, 2, 4, 2, 5};
// 値が2の要素を取り除く(ように移動させる)
auto new_end = std::remove(v.begin(), v.end(), 2);
// 実際にサイズを縮小する
v.erase(new_end, v.end());
for (int x : v) {
std::cout << x << " ";
}
return 0;
}
なぜこの二段階の手順が必要なのでしょうか。
それは、std::vector::eraseを繰り返すと、その都度要素のシフトが発生し計算量が O(N^2) になる恐れがあるからです。
一方、std::remove + erase の組み合わせであれば、計算量は O(N) で済み、非常に高速です。
C++20 以降の標準:std::erase と std::erase_if
C++20では、前述のErase-Removeイディオムをより簡潔かつ安全に記述するための非メンバ関数 std::erase および std::erase_if が導入されました。
直感的な記述が可能に
これにより、わざわざイテレータを操作したり、begin() や end() を記述したりする必要がなくなりました。
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6};
// 特定の値を削除 (C++20)
std::erase(v, 3);
// 条件に一致する要素を削除 (C++20)
std::erase_if(v, [](int n) { return n % 2 == 0; });
for (int x : v) {
std::cout << x << " ";
}
return 0;
}
1 5
この関数の登場により、「うっかり erase の引数を間違える」といったケアレスミスを防げるようになりました。
現代的なC++開発においては、可能な限りこちらの使用が推奨されます。
パフォーマンスを最適化するためのポイント
std::vectorの要素削除において、パフォーマンスを最大化するためのテクニックをいくつか紹介します。
末尾要素の削除(pop_back)
もし削除したい要素が常に末尾であるなら、erase ではなく pop_back() を使用してください。
これは O(1) で動作し、要素の移動が発生しません。
順序を保持しない高速削除(Swap and Pop)
もし要素の並び順が重要でない場合、特定の要素を削除する最も速い方法は、「削除したい要素を末尾要素とスワップ(入れ替え)してから pop_back() する」という手法です。
void quick_erase(std::vector<int>& v, size_t index) {
if (index < v.size()) {
// 削除したい要素を末尾と入れ替える
std::swap(v[index], v.back());
// 末尾を削除(移動が発生しない)
v.pop_back();
}
}
この手法は、要素数が数万件を超えるような std::vector から頻繁に要素を取り除くゲームエンジンや物理シミュレーションなどの分野でよく用いられます。
メモリ容量の再利用
erase を行っても、std::vector の内部的なメモリ容量(capacity)は減少しません。
メモリを完全に解放したい場合は、shrink_to_fit() を呼び出すことを検討してください。
各手法の比較まとめ
これまでに紹介した手法の特徴を以下の表にまとめました。
| 手法 | 適したケース | C++バージョン | 計算量 |
|---|---|---|---|
v.erase(it) | ループ内で特定の条件に基づき削除する場合 | 全世代 | O(N) |
| Erase-Removeイディオム | C++17以前で特定の値を一括削除する場合 | C++98〜 | O(N) |
std::erase / erase_if | モダンな環境での条件一致一括削除 | C++20〜 | O(N) |
| Swap and Pop | 順序不問で高速に削除したい場合 | 全世代 | O(1) |
よくある質問(FAQ)
Q: vector::clear() と v.erase(v.begin(), v.end()) に違いはありますか?
基本的には同じ結果になりますが、clear() の方が意図が明確であり、実装的にも最適化されている可能性が高いです。
全要素を削除する場合は clear() を使いましょう。
Q: std::remove_if を使った後に erase を忘れるとどうなりますか?
コンテナのサイズが変わりません。
末尾に「ゴミ」のようなデータが残ったままになり、論理的なバグの原因となります。
このミスを防ぐためにも、C++20が使えるなら std::erase_if を使うのが安全です。
まとめ
C++の std::vector における要素削除は、用途に応じて適切な方法を選択することが重要です。
- 特定の1要素を消すなら
eraseメンバ関数。 - ループ内で消すなら 戻り値のイテレータを適切に受け取る。
- 条件に合うものを一括で消すなら C++20の
std::erase_if。 - パフォーマンスを極限まで追求し、順序を問わないなら Swap and Pop。
特に、C++20以降は std::erase 系関数の導入により、安全で読みやすいコードが書けるようになりました。
古い教材やプロジェクトでは Erase-Remove イディオムが多く見られますが、新しいコードを書く際は最新の標準機能を積極的に取り入れ、バグの少ない効率的なプログラムを目指しましょう。
