std::vectorの要素のうち特定の条件に合致するものを削除する
C++ でstd::vector
のようなコンテナから特定の条件を果たす要素を削除したい場合はどうしますか?
例えば、数字が入っているstd::vector
からすべての偶数の要素を削除したい時はどうしますか?
まずはfor
ループを使うと思います。
std::vector<int> numbers{3, 20, 14, 2, 5, 90};
for (int i = 0; i < numbers.size(); ++i)
{
if (numbers[i] % 2 == 0)
{
numbers.erase(numbers.begin() + i);
}
}
for (const auto number : numbers)
{
std::cout << number << std::endl;
}
でもこれだと、{3, 14, 5}
が残ってしまいます。なんで?
i == 1
の時に20
を削除すると、numbers
の構造が変わって{3, 14, 2, 5, 90}
になり、次i == 2
になると、14
を飛ばして2
をチェックすることになります。
ここでたまに見かけるのは、以下の小技です。
for (int i = numbers.size(); i >= 0; --i)
{
if (numbers[i] % 2 == 0)
{
numbers.erase(numbers.begin() + i);
}
}
後ろから削除していくと、要素を飛ばしたりするリスクはないです。これが一番簡単で分かりやすいという人もいると思います。
因みにイテレーターを使うとどうなりますか?
for (auto it = numbers.begin(); it != numbers.end(); ++it)
{
if (*it % 2 == 0)
{
numbers.erase(it);
}
}
これだと、プログラムが SIGSEGV でクラッシュします。なぜかとうと、erase
をコールした後は、削除された要素を指していたit
はもう使えなくなってしまいます。なのに、++it
で次のイテレーターに移ろうとすると、クラッシュします。
なので、イテレーターを使う時は、以下のように削除しない場合は++it
で次の要素に移って、削除する場合はerase
の戻り値である削除後の次のイテレーターをit
に代入した方がいいです。これだと、クラッシュせずに済みます。
// for文中の++itは無くなってます
for (auto it = numbers.begin(); it != numbers.end();)
{
if (*it % 2 == 0)
{
it = numbers.erase(it);
}
else
{
++it;
}
}
でも上記のようなリスクがあるため、やはり基本的にfor
ループでstd::vector
を回す時は、そのstd::vector
から要素を削除したり追加したりしない方がいいです。
ここで私のお勧めは、for
ループを使わずにErase-remove idiom
という名のerase
とstd::remove_if
のコンビネーション技を使うことです。
numbers.erase( // 要素の削除にはerase()が必要
std::remove_if(
numbers.begin(), numbers.end(),
[](const auto& number) { return number % 2 == 0; }
),
numbers.end()
);
C++20 からはこの2つの動作を一気にやってくれるstd::erase_if
という関数が追加されて、ようやく以下のように1つの関数でできるようになりました。
std::erase_if (numbers, [](auto const& number) { return number % 2 == 0; });
なんで最初からこういう関数を用意してくれなかったのでしょうね・・・。
Discussion