🤷

vector/mapのアクセスは[]?それともat()?

に公開
2

今回はちょっと初心に帰って、C++ の[]演算子とat()関数の違いについて話したいと思います。同じに思えてもちょっと違うところがあったりします。

配列とstd::vectorの場合

配列とstd::vectorのようなシーケンスコンテナの場合は、ほぼ同じ挙動になるため、以下のような同じ使い方で問題ないです。

std::vector<int> data;

data.push_back(2);
std::cout << data.at(0) << std::endl;
std::cout << data[0] << std::endl;

唯一の違いは、[]演算子はインデックスのチェックをせずにデータにアクセスしようとするため、不正なインデックスを使うと、セグメンテーション違反になります。インデックスのチェックは完全にプログラマーの責任になります。
at()はインデックスのチェックをしてくれて、インデックスが不正な場合は、存在しない要素にアクセスをせずに「std::out_of_range」という例外を投げてくれます。例外をキャッチしない場合は、std::terminate() が呼び出されることでプログラムが中断されます。

std::vector<int> data;
std::cout << data.at(0) << std::endl; // std::out_of_range 例外
std::vector<int> data;
std::cout << data[0] << std::endl; // 不正なアクセス(sigsegv)

std::map と std::unordered_map の場合

std::mapstd::unordered_mapの場合は、[]演算子とat()関数の挙動が全然違ってきます。
読み出しアクセスするだけだったら、以下のように同じ使い方で良いのですが、

std::map<int, std::string> data{ {0, "test"} };

std::cout << data[0] << std::endl;
std::cout << data.at(0) << std::endl;

不正なインデックスの場合、at()はインデックスのチェックをしてくれて、「std::out_of_range」例外を投げてくれますが、[]演算子の挙動はどうでしょうか?

std::map<int, std::string> data;
// まだdataには1つもデータがない状態
std::cout << data.at(0) << std::endl; // std::out_of_range exception
std::cout << data[0] << std::endl; // クラッシュしない。空文字列が出力される!!

あれ?クラッシュしないのですか?
そうなのです。std::mapstd::unordered_mapの場合の[]演算子はアクセスするだけではないのです。アクセスしようとしているインデックスのデータが存在しない場合は、初期値で追加されてしまうのです。
そうしないと、以下のように新しい値を代入する時にクラッシュしてしまいます。

// あたらしく0というキーに対して値"test"を設定する
data[0] = "test";

右辺の"test"が代入される前にdata[0]で既に要素が追加されているのは他のクラスで使う[]演算子と全く違う仕様です。

std::map<int, std::string> data;
std::cout << data.size() << std::endl;  // 0
data[0];  // キー0に対しての要素追加が行われる
std::cout << data.size() << std::endl;  // 1

恐らくstd::mapに簡単に代入ができるようにするために、こういう挙動にしたのだと思います。
これがなかったら、insert()emplace()という正直少し使いづらい関数を使わないといけなくなります。

因みに、上記のような代入はat()でも以下のようにできますが、既にそのインデックスに該当する要素が存在する場合のみ代入が成功し、それ以外の場合はやはり「std::out_of_range」例外が投げられます。

// 既存の0というキーに対して"test"を設定する
data.at(0) = "test";

こういう副作用があるから、[]演算子を使わずに、アクセスにはat()を使い、要素の追加にはinsert()emplace()を使うことをお勧めするプログラマーもいます。(こういう初見殺しの罠が沢山置いてあるから、C++ は難しいと言われるのだろうな…)
そのため、C++17 でinsert_or_assignという関数が追加されました。挙動は[]演算子とほぼ同じですが、名前で挙動が明白になっています。インデックスに該当する値がなかったら、insertしますが、既に存在していたら、assignします。

data.insert_or_assign(0, "test");

[]演算子を使わない方が良いまでは言わないですが、使う際は注意しないといけない点があることを気に留めておいてください。


|cpp記事一覧へのリンク|

Discussion

yaito3014yaito3014

例外をキャッチしない場合は最終的に同じくセグメンテーション違反になります。

について、例外がスローされた時点で(存在しない)要素へのアクセスは阻害されているはずで、例外をキャッチしないことによるプログラムの中断は std::terminate が呼ばれることによるものです。
当該箇所は、例外をキャッチしない場合常にセグメンテーション違反が起こるという記述に読めてしまいます。

スペース・ソルバ株式会社スペース・ソルバ株式会社

ご指摘ありがとうございます!確かにその通りです。
例外補足については明示的に記載するようにしております。

また、operator[]とat()の例を連続で書いてしまっていた点も誤解を生むかと思いましたので、それぞれコードブロックを分けて記載するようにいたしました。