if (score["carol"] == 0)と書けば、「carolの点数が0なら」という条件になります。ところが、この1行を実行したあとのmapには、carolという要素が増えています。調べただけのつもりのコードが、データを書き換えている、というのが、mapの[]の動きです。
どんな場面で、何が起きるのかを、mapとunordered_mapで動かして確かめました。調べるだけのときに使う道具(find・at・contains)と、登録するときに使う道具(insert・emplace・try_emplace・insert_or_assign)の違いも、コンストラクタの呼ばれた回数で比べています。
検証は、Visual Studio 2022付属のMSVC(19.44、x64)を、Release(/O2 /MD)で動かして行いました。containsを使う実験だけ、C++20でビルドしています。
mapのoperator[]とは?
mapの[]は、次のように動きます。
・キーが無いときは、値を既定値で作って、要素として追加してから、その値への参照を返す
・キーがあるときは、既存の値への参照を返す
つまり、[]は「調べる」道具ではなく、「無ければ作って返す」道具です。値を読むためだけに使うと、読んだキーが、ぜんぶ登録されます。
実験①:3人を調べたら、要素が4件になった
2人(alice、bob)が入ったmapで、いない人も含めて3人の点数を読みます。「いない人は0点」のつもりで、[]を使いました。
▼e1_bracket.cpp
// operator[] で「読んだだけ」のつもりが、要素が増える
#include <cstdio>
#include <map>
#include <string>
#include <unordered_map>
int main()
{
std::map<std::string, int> score = {{"alice", 80}, {"bob", 65}};
std::printf("最初 : size = %zu\n", score.size());
// 「いない人は、0点」のつもりで、3人を調べる
int a = score["carol"];
int b = score["dave"];
int c = score["alice"];
std::printf("score[\"carol\"] = %d, score[\"dave\"] = %d, score[\"alice\"] = %d\n", a, b, c);
std::printf("[]で3人を調べたあと : size = %zu\n", score.size());
for (const auto& kv : score)
std::printf(" %-6s -> %d\n", kv.first.c_str(), kv.second);
std::map<std::string, int> score2 = {{"alice", 80}, {"bob", 65}};
bool hasCarol = score2.find("carol") != score2.end();
size_t daveCount = score2.count("dave");
std::printf("find / count で調べたあと: size = %zu (carol: %s, dave: %zu 件)\n", score2.size(), hasCarol ? "いる" : "いない", daveCount);
std::unordered_map<std::string, int> u = {{"alice", 80}};
int d = u["erin"];
std::printf("unordered_map でも : size = %zu (u[\"erin\"] = %d)\n", u.size(), d);
return 0;
}最初 : size = 2
score["carol"] = 0, score["dave"] = 0, score["alice"] = 80
[]で3人を調べたあと : size = 4
alice -> 80
bob -> 65
carol -> 0
dave -> 0
find / count で調べたあと: size = 2 (carol: いない, dave: 0 件)
unordered_map でも : size = 2 (u["erin"] = 0)最初はsize = 2でしたが、[]で3人を調べたあとは、size = 4になりました。aliceは、もともといたので増えず、いなかったcarolとdaveの2人が、点数0で、登録されています。
findとcountで調べたときは、size = 2のまま、増えませんでした。unordered_mapでも、u["erin"]を読んだだけで、要素が作られています(size = 2)。
【重要】実験②:[]で調べたせいで、nullptrが残って、あとで落ちる
値がポインタのmapで、「ドラゴンはいるか」を[]で調べると、さらに困ったことが起きます。
▼e2_ghost.cpp
// [] で調べたせいで、nullptr の「幽霊」が残り、あとで落ちる
#include <cstdio>
#include <cstring>
#include <map>
#include <string>
struct Enemy { std::string name; int hp; };
int main(int argc, char** argv)
{
std::setvbuf(stdout, nullptr, _IONBF, 0);
const bool fixed = argc > 1 && std::strcmp(argv[1], "fixed") == 0;
std::map<std::string, Enemy*> enemies;
enemies["slime"] = new Enemy{"slime", 10};
enemies["goblin"] = new Enemy{"goblin", 30};
// 「ドラゴンはいるか」を調べる
if (!fixed)
{
if (enemies["dragon"] != nullptr)
std::printf("ドラゴンがいる\n");
}
else
{
auto it = enemies.find("dragon");
if (it != enemies.end() && it->second != nullptr)
std::printf("ドラゴンがいる\n");
}
std::printf("登録数 = %zu\n", enemies.size());
for (const auto& kv : enemies)
std::printf("%s の HP = %d\n", kv.first.c_str(), kv.second->hp); // nullptr の要素で落ちる
return 0;
}| 調べ方 | 登録数 | 結果 |
|---|---|---|
enemies["dragon"] != nullptr([]) | 3(2体のはずが、増えた) | ループの途中で、0xC0000005で落ちた |
find("dragon")で、存在を確かめる | 2 | goblinとslimeのHPが出て、正常終了 |
[]のほうは、ドラゴンというキーに、nullptrが登録されました。そのあと、全員のHPを表示するループが、nullptrの要素に当たって、kv.second->hpでアクセス違反になっています。(std::mapは、キーの順に並ぶので、dragonが、先頭に来ました。)落ちる場所は、調べた場所ではなく、まったく別の、ループです。そのため、原因を探すのに、時間がかかります。
findで調べたほうは、登録数が2のまま、正常に終わりました。落ちたときのアドレスの読み方は、アクセス違反の記事で扱っています。
実験③:存在しないキーを調べ続けると、mapが太る
[]で調べるたびに、要素が増えるなら、存在しないキーを、たくさん調べると、どうなるでしょうか。map<int, int>で、存在しないキーを100万回調べました。
▼e4_cost.cpp
// 存在しないキーを [] で調べ続けると、map が太る
#include <windows.h>
#include <psapi.h>
#include <chrono>
#include <cstdio>
#include <map>
#include <string>
#pragma comment(lib, "psapi.lib")
static double workingSetMB()
{
PROCESS_MEMORY_COUNTERS pmc{};
GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc));
return pmc.WorkingSetSize / (1024.0 * 1024.0);
}
static volatile int g_sink = 0;
int main()
{
const int N = 1000000;
std::map<int, int> byBracket, byFind;
byBracket[0] = 1; byFind[0] = 1;
double mem0 = workingSetMB();
auto t0 = std::chrono::steady_clock::now();
int s = 0;
for (int i = 1; i <= N; ++i) s += byBracket[i]; // どれも存在しないキー
auto t1 = std::chrono::steady_clock::now();
g_sink = s;
double mem1 = workingSetMB();
std::printf("[] で、無いキーを %d 回調べた : size = %7zu, %7.1f ms, メモリ %+6.1f MB\n", N, byBracket.size(),
std::chrono::duration<double, std::milli>(t1 - t0).count(), mem1 - mem0);
auto t2 = std::chrono::steady_clock::now();
s = 0;
for (int i = 1; i <= N; ++i) { auto it = byFind.find(i); if (it != byFind.end()) s += it->second; }
auto t3 = std::chrono::steady_clock::now();
g_sink = s;
double mem2 = workingSetMB();
std::printf("find で、無いキーを %d 回調べた: size = %7zu, %7.1f ms, メモリ %+6.1f MB\n", N, byFind.size(),
std::chrono::duration<double, std::milli>(t3 - t2).count(), mem2 - mem1);
return 0;
}[] で、無いキーを 1000000 回調べた : size = 1000001, 125.6 ms, メモリ +46.1 MB
find で、無いキーを 1000000 回調べた: size = 1, 1.0 ms, メモリ +0.1 MB![mapに存在しないキーを100万回調べたときの、時間と増えたメモリ。[]とfindの比較](https://kashiroslog.com/wp-content/uploads/2026/11/map_operator_bracket_insert_cost.png)
[]は、100万個の要素を作ってしまい、sizeは1000001。かかった時間は約125.6ミリ秒で、増えたメモリは、約46.1MBでした。findは、要素を作らないので、約1.0ミリ秒、メモリの増加は、ほぼ0です。存在しないキーが、頻繁に来る処理(キャッシュの判定や、ゲームの、ID照会など)では、[]が、メモリを、じわじわ食います。
実験④:constのmapには、[]が使えない
値を読むだけの関数の引数を、constの参照にした場合、[]で、ビルドが通りません。
▼e5_const_bad.cpp
// const の map に、[] は使えない
#include <map>
#include <string>
int lookup(const std::map<std::string, int>& m, const std::string& key)
{
return m["alice"]; // ビルドが通らない
}
int main()
{
std::map<std::string, int> m = {{"alice", 80}};
return lookup(m, "alice");
}e5_const_bad.cpp(7): error C2678: 二項演算子 '[': 型 'const std::map<std::string,int,std::less<std::string>,std::allocator<std::pair<const std::string,int>>>' の左オペランドを扱う演算子が見つかりません (または変換できません) (新しい動作; ヘルプを参照)。
e5_const_bad.cpp(7): note: 変換で修飾子が失われます。C2678で、constのmapの[]は、見つからないと言われました。[]は、要素を追加する可能性があるため、constのオブジェクトに対しては、呼べないからです。「読むだけの関数で、[]を書いたら、constが付けられなくなった」場合は、その[]が、本当は、読むだけではなかった、というサインです。
調べるだけの道具:find・contains・at
▼e6_lookup.cpp
// 「調べるだけ」の道具: find / count / contains / at
#include <cstdio>
#include <map>
#include <stdexcept>
#include <string>
int main()
{
std::map<std::string, int> score = {{"alice", 80}, {"bob", 65}};
std::printf("contains(\"alice\") = %s / contains(\"carol\") = %s\n",
score.contains("alice") ? "true" : "false", score.contains("carol") ? "true" : "false");
auto it = score.find("bob");
if (it != score.end())
std::printf("find(\"bob\") = %d\n", it->second);
try
{
std::printf("at(\"carol\") = ");
std::printf("%d\n", score.at("carol"));
}
catch (const std::out_of_range& e)
{
std::printf("out_of_range 例外 (%s)\n", e.what());
}
std::printf("調べたあとの size = %zu\n", score.size());
score.try_emplace("carol", 50); // 無ければ追加
score.try_emplace("alice", 0); // あれば、何もしない
std::printf("try_emplace のあと : alice=%d carol=%d size=%zu\n", score.at("alice"), score.at("carol"), score.size());
return 0;
}contains("alice") = true / contains("carol") = false
find("bob") = 65
at("carol") = out_of_range 例外 (invalid map<K, T> key)
調べたあとの size = 2
try_emplace のあと : alice=80 carol=50 size=3| 書き方 | 無いキーを調べたとき | 要素は増えるか |
|---|---|---|
m[key] | 既定値で要素を作って返す | 増える |
m.find(key) | end()が返る(it != m.end()で判定) | 増えない |
m.count(key) | 0が返る | 増えない |
m.contains(key)(C++20) | falseが返る | 増えない |
m.at(key) | out_of_range例外(invalid map<K, T> key) | 増えない |
「いるか」だけ知りたいなら、contains(C++17以前ならcount)。「いれば値を使いたい」なら、findです。「必ずいるはず」の場面なら、atにしておくと、いないときに、その場で例外になるので、気づけます。
実験⑤:登録・上書きの道具で、何が違うか
登録する側にも、道具がいくつかあります。値の型に、「既定コンストラクタ」「整数からの構築」「コピー」「ムーブ」「代入」の回数を数えるクラス(Probe)を使い、キーが、無いときと、あるときで、回数を比べました。
▼e3_methods.cpp
// 登録・上書きの方法ごとに、値の型のコンストラクタが何回呼ばれるか
#include <cstdio>
#include <map>
#include <string>
#include <utility>
struct Probe
{
int v = 0;
static int dflt, make, copy, move, assign;
Probe() { ++dflt; }
Probe(int x) : v(x) { ++make; }
Probe(const Probe& o) : v(o.v) { ++copy; }
Probe(Probe&& o) noexcept : v(o.v) { ++move; }
Probe& operator=(const Probe& o) { v = o.v; ++assign; return *this; }
Probe& operator=(Probe&& o) noexcept { v = o.v; ++assign; return *this; }
static void reset() { dflt = make = copy = move = assign = 0; }
};
int Probe::dflt = 0, Probe::make = 0, Probe::copy = 0, Probe::move = 0, Probe::assign = 0;
static void report(const char* label, const std::map<int, Probe>& m, int key)
{
std::printf("%-32s 既定 %d / 整数から %d / コピー %d / ムーブ %d / 代入 %d → 値は %d\n", label, Probe::dflt, Probe::make, Probe::copy, Probe::move, Probe::assign, m.at(key).v);
}
int main()
{
std::printf("【キーが、まだ無いとき】(値 7 を登録)\n");
{
std::map<int, Probe> m; Probe::reset();
m[1] = Probe(7); report("m[1] = Probe(7)", m, 1);
}
{
std::map<int, Probe> m; Probe::reset();
m.insert({1, Probe(7)}); report("m.insert({1, Probe(7)})", m, 1);
}
{
std::map<int, Probe> m; Probe::reset();
m.emplace(1, 7); report("m.emplace(1, 7)", m, 1);
}
{
std::map<int, Probe> m; Probe::reset();
m.try_emplace(1, 7); report("m.try_emplace(1, 7)", m, 1);
}
{
std::map<int, Probe> m; Probe::reset();
m.insert_or_assign(1, Probe(7)); report("m.insert_or_assign(1, Probe(7))", m, 1);
}
std::printf("\n【キーが、すでにあるとき】(元の値は 1 / 新しい値 7 を登録しようとする)\n");
{
std::map<int, Probe> m; m.emplace(1, 1); Probe::reset();
m[1] = Probe(7); report("m[1] = Probe(7)", m, 1);
}
{
std::map<int, Probe> m; m.emplace(1, 1); Probe::reset();
m.insert({1, Probe(7)}); report("m.insert({1, Probe(7)})", m, 1);
}
{
std::map<int, Probe> m; m.emplace(1, 1); Probe::reset();
m.emplace(1, 7); report("m.emplace(1, 7)", m, 1);
}
{
std::map<int, Probe> m; m.emplace(1, 1); Probe::reset();
m.try_emplace(1, 7); report("m.try_emplace(1, 7)", m, 1);
}
{
std::map<int, Probe> m; m.emplace(1, 1); Probe::reset();
m.insert_or_assign(1, Probe(7)); report("m.insert_or_assign(1, Probe(7))", m, 1);
}
return 0;
}【キーが、まだ無いとき】(値 7 を登録)
m[1] = Probe(7) 既定 1 / 整数から 1 / コピー 0 / ムーブ 0 / 代入 1 → 値は 7
m.insert({1, Probe(7)}) 既定 0 / 整数から 1 / コピー 0 / ムーブ 2 / 代入 0 → 値は 7
m.emplace(1, 7) 既定 0 / 整数から 1 / コピー 0 / ムーブ 0 / 代入 0 → 値は 7
m.try_emplace(1, 7) 既定 0 / 整数から 1 / コピー 0 / ムーブ 0 / 代入 0 → 値は 7
m.insert_or_assign(1, Probe(7)) 既定 0 / 整数から 1 / コピー 0 / ムーブ 1 / 代入 0 → 値は 7
【キーが、すでにあるとき】(元の値は 1 / 新しい値 7 を登録しようとする)
m[1] = Probe(7) 既定 0 / 整数から 1 / コピー 0 / ムーブ 0 / 代入 1 → 値は 7
m.insert({1, Probe(7)}) 既定 0 / 整数から 1 / コピー 0 / ムーブ 1 / 代入 0 → 値は 1
m.emplace(1, 7) 既定 0 / 整数から 0 / コピー 0 / ムーブ 0 / 代入 0 → 値は 1
m.try_emplace(1, 7) 既定 0 / 整数から 0 / コピー 0 / ムーブ 0 / 代入 0 → 値は 1
m.insert_or_assign(1, Probe(7)) 既定 0 / 整数から 1 / コピー 0 / ムーブ 0 / 代入 1 → 値は 7結果から、次のことが分かります。
m[1] = Probe(7)は、キーが無いとき、先に既定コンストラクタで値を作ってから、代入しました(既定1・代入1)。値の型に、既定コンストラクタが無いと、そもそも使えませんinsertは、キーがすでにあると、元の値のまま(1)でした。上書きは、されません。しかも、渡したProbe(7)は、無駄に作られていますtry_emplaceは、キーがあるとき、何も作らず、何もしません(整数からの構築も0回)insert_or_assignは、キーがあれば代入(上書き)、無ければ、追加します。「上書きしたい」ときの、意図が分かりやすい書き方です
emplaceは、このMSVCでは、キーがあるときに、何も作りませんでした。ただし、標準では、キーがあっても、要素を作る場合がある、と決められているので、環境によって、動きが変わる可能性があります。キーがあれば何もしないことを保証したいときは、try_emplaceを使います。
直し方:やりたいことで、道具を選ぶ
| やりたいこと | 使う書き方 |
|---|---|
| いるかどうかだけ、知りたい | contains(C++20)・count |
| いれば値を使い、いなければ何もしない | find+it != end() |
| 必ずいるはず。いなければバグ | at |
| 無ければ追加し、あれば何もしない | try_emplace |
| あってもなくても、この値にしたい | insert_or_assign(またはm[key] = value) |
出現回数を数える(++m[key]) | [](無ければ0から数え始めるのが目的なので、この場面は[]が便利) |
よくある失敗例と対処法
①「いるか」を、[]で確かめる
実験①②のとおり、調べたキーが、すべて登録されます。containsかfindにします。
②値がポインタのmapで、[]の結果を、nullptrと比べる
実験②のとおり、nullptrの要素が、残って、別の場所で落ちます。ポインタのmapでは、特に、findを使います。
③constの関数で、[]を使おうとして、constを外す
実験④のとおり、[]が通らないのは、書き換えが入っているサインです。constを外すのではなく、findかatに直します。
④insertで、上書きできると思い込む
実験⑤のとおり、insertは、キーがあると、上書きしません。上書きしたいなら、insert_or_assignか、m[key] = valueです。
注意点
[]は「調べる」道具ではなく、「無ければ作って返す」道具。調べただけで、要素が増える- 2人の
mapで、3人を[]で調べたら、sizeが4になった(unordered_mapでも同じ) - 値がポインタの
mapでは、nullptrが登録されて、別の場所のループで落ちた(0xC0000005) - 存在しないキーを100万回調べると、
[]は約125倍遅く、メモリが約46MB増えた constのmapでは[]が使えない(C2678)insertは上書きしない。try_emplaceは、キーがあれば何も作らない。insert_or_assignは上書きする- 今回の検証は、MSVC 19.44(Visual Studio 2022付属、x64、
/O2 /MD)で行った。emplaceの細かい動きは、実装で異なる場合がある
この記事のポイント
- 調べるだけなら、
[]を使わない。contains・find・atを使う []が向いているのは、「無ければ既定値から始めたい」場面。出現回数の集計(++m[key])が、その例- 登録は、やりたいことに合わせて、
try_emplaceとinsert_or_assignを選ぶ constが付けられない[]は、「書き換えている」というサイン
[]は、短く書けて、便利です。便利な道具ほど、「読んだだけ」に見える行が、裏で何を書き換えているのかを、一度、実行結果で見ておくと、使いどころが、はっきりします。


