Code Fix

上級

vectorの要素をループ中に削除するとイテレータが無効化されるバグ

std::vectorの要素をfor文の中でeraseしながら、そのままイテレータをインクリメントし続けてしまうバグです。eraseは削除した位置以降の要素を前に詰めるため、単純な++itでは次の要素を読み飛ばしてしまいます。

なぜエラーが出ないのか

出力: 4 1 3 (2番目の偶数4が消えずに残る)
(エラーなし)
C++は何も報告しません。コンパイルも実行も正常に完了するためです
出力: 4 1 3 (2番目の偶数4が消えずに残る)
実際の挙動 — 期待した結果と食い違っている箇所
見つけ方
エラーが出ないので、出力を目で確かめるか、コードを目で追うしかありません。この種の誤りが最も発見が遅れます

このエラーが出る典型パターン

パターン1

 1  #include <iostream>
 2  #include <vector>
 3  int main() {
 4      std::vector<int> nums = {2, 4, 1, 3};
 5      for (auto it = nums.begin(); it != nums.end(); ++it                                         ) {
                                                                             ^
 6          if (*it % 2 == 0) {
 7              nums.erase(it);
 8          }
 9      }
10      for (int n : nums) std::cout << n << " ";
11      std::cout << std::endl;
12      return 0;
13  }
出力: 4 1 3 (2番目の偶数4が消えずに残る)期待: 1 3

eraseは削除した位置以降の要素を前に詰め、削除した位置を指す新しいイテレータを返り値として返します。それを使わずに自分で++itすると、詰められて繰り上がってきた次の要素を読み飛ばしてしまいます。eraseの戻り値を使ってitを更新するのが正しい書き方です。

直し方: ++itit = (*it % 2 == 0) ? nums.erase(it) : it + 1 にします。

この問題を解いてみる →

広告
広告スロット(未設定)

パターン2

 1  #include <iostream>
 2  #include <vector>
 3  int main() {
 4      std::vector<int> vals = {6, 8, 1, 5};
 5      for (auto it = vals.begin(); it != vals.end(); ++it                                         ) {
                                                                             ^
 6          if (*it % 2 == 0) {
 7              vals.erase(it);
 8          }
 9      }
10      for (int v : vals) std::cout << v << " ";
11      std::cout << std::endl;
12      return 0;
13  }
出力: 8 1 5 (2番目の偶数8が消えずに残る)期待: 1 5

「イテレータはコンテナの中身が変わっても位置を追いかけてくれる」というのは誤解です。vector::eraseは呼んだ時点でそのイテレータ自身を無効にします。

直し方: ++itit = (*it % 2 == 0) ? vals.erase(it) : it + 1 にします。

この問題を解いてみる →

パターン3

 1  #include <iostream>
 2  #include <vector>
 3  int main() {
 4      std::vector<int> nums = {10, 12, 3, 9};
 5      for (auto it = nums.begin(); it != nums.end(); ++it                                         ) {
                                                                             ^
 6          if (*it % 2 == 0) {
 7              nums.erase(it);
 8          }
 9      }
10      for (int n : nums) std::cout << n << " ";
11      std::cout << std::endl;
12      return 0;
13  }
出力: 12 3 9 (2番目の偶数12が消えずに残る)期待: 3 9

単純なfor文の++itだけで削除を扱おうとすると1つ余分に読み飛ばします。vectorから複数条件で削除するときは、eraseの戻り値でイテレータを更新するか、std::remove_ifとの組み合わせ(消去・削除イディオム)を使うのが安全です。

直し方: ++itit = (*it % 2 == 0) ? nums.erase(it) : it + 1 にします。

この問題を解いてみる →

よくある誤解

「イテレータはコンテナの中身が変わっても位置を追いかけてくれる」というのは誤解です。vector::eraseは呼んだ時点でそのイテレータ自身を無効にし、代わりに次の要素を指す新しいイテレータを戻り値として返します。それを使わずに自分でインクリメントすると、1つ余分に読み飛ばしてしまいます。

まとめ

vectorの要素をループ中に削除するとイテレータが無効化されるバグは上級でつまずきやすい項目です。上の3パターンを実際に手で直すと、エラーメッセージのどこを読めばよいかが掴めます。

演習をはじめる

関連するエラー

広告
広告スロット(未設定)