第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::map | std::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 が読みにくい。