C++ Learning

第18回 std::map / std::unordered_map ― キーと値の組

「名前 → 点数」「ID → 商品」のようなキーと値の対応表を扱うコンテナ。Python の dict、JavaScript のオブジェクトに相当します。C++ には 2 種類あり:キーをソートして保持する std::map(平衡二分木)と、ハッシュで散らばらせる std::unordered_map(ハッシュテーブル)。用途に応じて使い分けます。

このページで押さえること
✅ 最低限ここだけ覚える
  • std::map<K, V> でキー → 値の対応表
  • 挿入は m[key] = value;
  • 取り出しは m[key]m.at(key)
  • 存在確認は m.count(key)m.contains(key) (C++20)
⭐ 余裕があれば読む
  • map は O(log n)、unordered_map は O(1) 平均
  • m[key] は存在しないと自動で挿入(罠)
  • find()end() で見つからない判定
  • 範囲 for + 構造化束縛が相性抜群

1. まず触ってみる ― 名前から点数を引く

std::map は、キーから値を素早く取り出せる対応表です。たとえば「生徒の名前」をキーに「点数」を値として保存すると、後から 名前を指定して点数を取り出せる

▶ 生徒の点数を管理

まずは 4 行だけ

first_map.cppC++ 最小例
#include <map> #include <string> std::map<std::string, int> scores; scores["Alice"] = 85; // 追加 scores["Bob"] = 72; std::cout << scores["Alice"]; // 85 を取り出す
ここまでで覚えること(3 つ):
  • std::map<キーの型, 値の型> で作る
  • m[key] = value; で追加、m[key] で取得
  • 使用前に #include <map>

よくある素朴な疑問

Q. Python の dict と何が違う?
→ 概念は同じです。違いは (1) キーの型が固定(dict のように何でも入れられない)、(2) std::mapキー順でソートされる、(3) 存在しないキーにアクセスすると自動で挿入される(罠)。

Q. std::unordered_map との違いは?
mapキー順でソート保持(木構造)、unordered_mapハッシュで保存(順序不明)。検索は unordered_map のほうが速いですが、順番が欲しいときは map を。詳しくは §4 で。

Q. キーに使える型は?
map は「< で比較できる型」なら何でも(int, double, string, pair, 自作クラスも可)。unordered_map は「ハッシュ関数がある型」(int/string などは最初から OK、自作クラスは自分で書く必要あり)。

2. 基本操作(挿入・取得・削除)

① 挿入(追加)

[] で挿入日常
std::map<std::string, int> m; m["apple"] = 100; // 追加 → {"apple":100} m["banana"] = 200; // 追加 → {"apple":100, "banana":200} m["apple"] = 150; // 上書き → {"apple":150, "banana":200}
insert / emplace明示的
m.insert({"cherry", 300}); // 追加 // 既にキーがあると『挿入せず失敗』 // ([] との違い) m.emplace("grape", 500); // その場で構築

② 取得

[] で取得罠あり
int x = m["apple"]; // 150 // ⚠ 存在しないキーに [] int y = m["lemon"]; // 0 (int のデフォルト値) // しかも m に {"lemon":0} が勝手に挿入される!
at で取得安全
int x = m.at("apple"); // 150 // 存在しないキーは例外 m.at("lemon"); // std::out_of_range // 挿入はされない

③ 存在確認

count / contains推奨
if (m.count("apple") > 0) { // 存在する } // C++20 以降はより読みやすい if (m.contains("apple")) { // 存在する }
find + endイテレータ版
auto it = m.find("apple"); if (it != m.end()) { std::cout << it->second; } // 同時に「存在確認 + 値取得」したいときに便利

④ 削除

erase削除
m.erase("apple"); // キー指定で削除 // 戻り値: 削除された要素数(0 か 1) m.clear(); // 全消去 if (m.erase("xxx") == 0) { // 存在しなかった }

3. ループで回す(構造化束縛)

map をループで回すとき、第 5 回で触れた範囲 for と 第 12 回の構造化束縛が絶大な効果を発揮します。

C++11 時代first/second
for (const auto& p : m) { std::cout << p.first << ": " << p.second << "\n"; }
C++17 以降構造化束縛
for (const auto& [name, score] : m) { std::cout << name << ": " << score << "\n"; }
std::mapキー順でイテレートされます。「Alice → Bob → Charlie」のようにソート済みの順で出てくる。unordered_map は順序が保証されないので、注意。

値だけ/キーだけ取り出す

値だけ_ で無視
for (const auto& [_, score] : m) { std::cout << score << ' '; } // ※ _ は慣習。実際は未使用警告が出ないよう // [[maybe_unused]] を付けるのが厳密
キーだけ同様
for (const auto& [name, _] : m) { std::cout << name << ' '; }

4. map vs unordered_map の使い分け

同じような機能ですが、内部実装が全く違います。

項目std::mapstd::unordered_map
内部構造平衡二分木(赤黒木)ハッシュテーブル
検索・挿入・削除O(log n)O(1) 平均(最悪 O(n))
イテレート順キー順(ソート済み)順序不定
キーの型要件< で比較可能ハッシュ関数 + ==
メモリ使用量少なめ多め(ハッシュテーブル用の余裕)
範囲検索(a〜b のキー)得意不可
判断フロー:
  • キー順にイテレートしたい → std::map
  • 範囲検索(ある値以上のキーを全部など)が要る → std::map
  • とにかく速く検索したい/順序不要 → std::unordered_map
  • 迷ったらstd::map(順序付きで直感的、デバッグもしやすい)

使い方はほぼ同じ

入れ替え可能API 共通
#include <unordered_map> std::unordered_map<std::string, int> m; m["apple"] = 100; std::cout << m.at("apple"); // map とほぼ同じコード
ここまでで map の日常は OK
残りは [] の落とし穴。ハマりやすいので読んでおくと安心。

5. よくある罠

罠 1: [] は存在しないキーを自動で作る

落とし穴意図しない挿入
std::map<std::string, int> m; m["apple"] = 100; if (m["banana"] == 0) { // ← ここで banana:0 が挿入される std::cout << "not found"; } std::cout << m.size(); // 2! apple と banana
OKcount / find
std::map<std::string, int> m; m["apple"] = 100; if (m.count("banana") == 0) { std::cout << "not found"; } std::cout << m.size(); // 1 (変わらず)

「存在チェック」なのに値を代入したような副作用が起きる、というバグの温床。存在確認は必ず count / contains / find を使いましょう。

罠 2: const std::map[] が使えない

NGconst で []
void show(const std::map<std::string, int>& m) { std::cout << m["apple"]; // コンパイルエラー } // [] は書き込みの可能性があるため // const map には使えない
OKat を使う
void show(const std::map<std::string, int>& m) { std::cout << m.at("apple"); // OK }

罠 3: イテレータ無効化

map のイテレータは、他の要素を erase しても無効にならないのが嬉しいところ(vector と違う)。ただし、自分が指している要素を erase するとそのイテレータは無効に。ループで条件に合う要素を削除するときは注意:

条件で削除idiom
for (auto it = m.begin(); it != m.end(); ) { if (it->second < 50) it = m.erase(it); // erase は次のイテレータを返す else ++it; }
C++20 以降なら std::erase_if が最もシンプル: std::erase_if(m, [](const auto& p){ return p.second < 50; });
広告スペース

確認クイズ

map / unordered_map を 4 問で確認。

Q1. std::map<std::string, int> m;m["apple"] = 100; した後、m["banana"] を読むとどうなる?

例外 std::out_of_range が投げられる
コンパイルエラー
0 が返り、しかも {"banana": 0} が挿入される
何も起きずに 0 が返る
[] は「キーがなければ値をデフォルト構築して挿入」する挙動。int なら 0 で挿入されます。存在確認には m.count(key)m.find(key) != m.end() を使いましょう。

Q2. std::map の検索の計算量は?

O(1)
O(n)
O(log n)
O(n log n)
std::map は平衡二分木(多くの実装で赤黒木)なので、挿入・検索・削除すべて O(log n)。O(1) 平均が欲しければ std::unordered_map(ハッシュテーブル)を使います。

Q3. std::unordered_map を選ぶべきケースは?

キー順にデータを出力したい
「100 以上のキー全部」のような範囲検索をしたい
大量のキーから最速で検索したい、順序は不要
キーが自作クラスで、< 演算子しかない
unordered_map は O(1) 平均で検索が速いのが魅力。順序が必要なら map、範囲検索が必要なら map、自作クラスのキーが順序しかないなら map。順序不要で検索高速化が目的なら unordered_map。

Q4. m の全要素を「名前: 点数」の形式で出力する最もモダンな書き方は?

for (int i = 0; i < m.size(); ++i) ...
for (auto it : m) std::cout << it.first ...
for (const auto& p : m) std::cout << p.first ...
for (const auto& [name, score] : m) std::cout << name ...
C++17 の構造化束縛と範囲 for の組み合わせが最も読みやすい。const auto& で要素をコピーせず、[name, score] で意味のある名前に分解。①はそもそも map はインデックスアクセスできない。②はコピーが発生。③は動くが .first/.second が読みにくい。