Menu

C++ std::map:キー、値、検索、挿入

C++ の std::map を解説 — 対数時間で検索できる、キーでソートされたキー・バリュー型コンテナ。挿入・検索・反復処理の方法と、キーをこっそり挿入してしまう operator[] の定番の落とし穴を回避する方法。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

キーでの検索

vector は、位置でインデックス指定するとき—要素 0、要素 1 など—に最適です。しかし多くの場合、あなたが持っているのは位置ではなく名前で、それに紐づくものが欲しいのです。ユーザー名とそのスコア、単語とその出現回数、国コードとその首都など。一致するものを探して vector を走査するのは O(n) で、すぐに遅くなります。

std::map がこれを解決します。キー・バリューのペアを格納し、キーでソートした状態に保ち、キーから値を O(log n) 時間で検索できます。使うには <map> をインクルードします。

map<string, int> は「string のキーから int の値への map」と読みます。キーは一意で、同じキーに二度代入すると後の値が勝ちます。内部的には map は平衡二分探索木であり、これがすべてがソートされたままになり、検索が定数ではなく対数時間になる理由です。

要素の挿入

エントリを入れる方法はいくつかあり、それらの違いが重要です。最も一般的なのは operator[] で、キーが存在しなければ作成し、代入できる参照を返します。

既存のキーの上書きを拒否する挿入が欲しい場合は、insertemplace を使います。どちらも pair を返し、その .second は挿入が実際に行われたかどうかを示す bool です。

「最後に書き込んだものが勝つ」のが望みなら [] を、既存のキーをそのままにしておきたいなら insert/emplace を使いましょう。

operator[] の落とし穴:読み取りで挿入される

これは最もよくある map のバグです。operator[] は純粋な読み取りではありません。キーが無いと、デフォルト構築された値(int なら 0string なら "" など)でこっそり挿入し、それへの参照を返します。したがって、[] でキーを単にチェックするだけでも map が変化します。

map<string, int> m;
if (m["maybe"] == 0) {   // バグ:これは "maybe" -> 0 を作成しただけ
    // ...
}
cout << m.size();        // 0 ではなく 1 - うっかりキーを挿入してしまった

これは const map でも問題になります。operator[] は挿入が必要になる可能性があるため、そもそもコンパイルできません。挿入せずに読み取るには、findcount/contains、または at を使います。

経験則:読み取るつもりなら、決して [] に手を伸ばさないこと。キーが必ず存在するべきときは at を、存在しないかもしれないときは find/contains を使いましょう。

ソート順での反復処理

map は木構造で実装されているため、反復処理ではキーを昇順にソートされた順序で—常に、無料で—たどります。各要素は pair<const Key, Value> なので、構造化束縛を使ってキーと値をすっきり取り出しましょう。

束縛のキーが const であることに注意してください。auto& を使えばループ内で値は変更できますが、キーをその場で変更することは決してできません(ソート順が壊れてしまうため)。出力はソートのステップを一切踏まずにアルファベット順(blueseasky)で出てきます。これこそ、順序付きの走査が重要なときにハッシュテーブルではなく map を選ぶ理由です。

この wordCount[w]++ というイディオムは、アクセス時挿入の振る舞いの典型的な使い方でもあります。ここでは存在しないキーを 0 から始めたいので、[] が正しい道具です。

要素の削除とサイズ

キーで削除するには erase を使い、削除された要素の数(map では 0 か 1)を返します。find から得たイテレータを使って削除することもできます。コンテナの状態は size()empty() で確認します。

ループ内で削除するときの落とし穴:m.erase(it)it を無効化するため、その後に ++it はできません。C++11 以降の安全なパターンは it = m.erase(it) で、次の要素へのイテレータを返します。map 全体にわたる一括の条件付き削除には、std::erase_if(m, predicate)(C++20)の方がすっきりします。

次へ:unordered_map

std::map はソートされたキーと予測可能な O(log n) の操作を提供しますが、その順序の代償をすべての検索で支払うことになります。キーの順序を気にせず、とにかく最速の検索が欲しいときは、unordered_map がソート済みの木をハッシュテーブルに置き換え—平均 O(1) のアクセスを実現します。次は、その仕組み、平均定数時間が map の対数を上回るのはいつか、そしてそれに伴うハッシュの落とし穴(カスタムキー型、最悪ケースの衝突)を見ていきます。

よくある質問

C++ の std::map とは何ですか?

std::map は、キーでソートされたキー・バリューのペアを格納する連想コンテナです。内部が平衡二分探索木で実装されているため、検索・挿入・削除はすべて O(log n) です。各キーは一意で、重複するキーを挿入しても既存の値は変わりません。

C++ の map で operator[] と at() の違いは何ですか?

map[key] は値への参照を返し、キーが存在しない場合はデフォルト構築された値でこっそり挿入します。map.at(key) も参照を返しますが、キーが無いときは挿入せずに std::out_of_range を投げます。読み取るだけのときは at()(または find())を使いましょう。

C++ の map にキーが存在するかどうかをどう確認しますか?

m.contains(key)(C++20)、0 か 1 を返す m.count(key)、または m.find(key) != m.end() を使います。m[key] でのテストは避けてください。キーが無いと挿入されてしまい、たいていは望まない動作です。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める