In a nutshell
C++における配列的な要素アクセスには,[i]
(添字演算子)と at(i)
(メンバ関数)の2つがある.どちらも似たように使えるが,安全性・エラーハンドリング・パフォーマンスの面で大きく異なる.両方の選択肢が利用できるのであれば, at(i)
を利用する方が安全である.
はじめに
C++ には,std::vector
や std::string
などのコンテナから要素を取得する方法として [i]
と at(i)
がある.普段は特に意識せず [i]
を使用していたが,at(i)
を使っているスクリプトを見た際に,この違いについて理解していない事に気が付いた.そこで,両者の違いについて,以下に纏めた.
予想される読み手
- C++に関して初学者である
基本的な仕様の違い(表と解説)
vec[i]
は配列のように要素へ直接アクセスでき,パフォーマンスも高いが,インデックスが有効かどうかのチェックが行われないため,無効なインデックスを指定した場合,未定義動作(クラッシュや意図しない動作)となる.一方で vec.at(i)
は,アクセス前にインデックスの範囲チェックを自動で行い,無効な場合は std::out_of_range
例外を投げる.そのため,安全に利用できるが,若干パフォーマンスに劣る.
機能 | [i] |
at(i) |
---|---|---|
範囲チェック | なし(未定義動作) | あり(範囲外で例外を投げる) |
戻り値型 | 要素の参照(読み書き可能) | 要素の参照(読み書き可能) |
例外発生 | なし | 範囲外で std::out_of_range
|
パフォーマンス | 高い | やや低い(範囲チェックあり) |
noexcept | yes | no(例外を投げる可能性あり) |
具体的な違いとコード例
[i]
の使用例
範囲外にアクセスしても,エラーを吐かずにバグを引き起こすことがある.したがって,検知されにくく,大変に厄介で危険である.
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {10, 20, 30};
std::cout << vec[1] << std::endl; // 出力: 20
std::cout << vec[3] << std::endl; // 範囲外 -> 未定義動作
}
at(i)
の使用例
at()
を使うことで,安全にエラー検知が可能.バグ等の回避に貢献する.
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {10, 20, 30};
try {
std::cout << vec.at(1) << std::endl; // 出力: 20
std::cout << vec.at(3) << std::endl; // 例外が発生
} catch (std::out_of_range& e) {
std::cerr << "範囲外アクセス: " << e.what() << std::endl;
}
}
使い分け
at(i)
を使うべき場面
- 安全性を重視する
- デバッグ中
- ユーザー入力など,外部からのインデックス指定がある
- 本番環境で不正アクセスを,明示的に防ぎたい
[i]
を使うべき場面
- パフォーマンスが極めて重要なケース
- アクセスするインデックスが常に正しいと保証されている
- 大量の要素に対してループ処理をする(速度優先)
注意点・例外的なこと
その他のコンテナとの関係
-
std::array
,std::string
もat()
を持つ. -
std::map
,std::unordered_map
のoperator[]
は,存在しないキーにアクセスすると新しい要素が追加される. -
at()
は同様の状況で例外を投げるため,意図しない挿入を防げる.
#include <map>
#include <iostream>
int main() {
std::map<std::string, int> mp;
mp["key1"] = 10;
std::cout << mp["key2"] << std::endl; // 0が挿入される
try {
std::cout << mp.at("key3") << std::endl; // 例外発生
} catch (std::out_of_range& e) {
std::cout << "存在しないキー: " << e.what() << std::endl;
}
}
特殊ケース・補足事項
const
に対する動作
読み取り専用でも [i]
, at(i)
の両方は使えるが,書き込みはできない.
const std::vector<int> v = {1, 2, 3};
int x = v[1]; // 読み取りのみ可能
int y = v.at(2); // 読み取りのみ可能
std::vector<bool>
の特殊性
std::vector<bool>
は特殊化されており,[i]
や at(i)
の戻り値は bool&
ではなくプロキシオブジェクトになる.このため,ポインタ取得や参照渡しなど一部の操作が制限される.
std::vector<bool> flags = {true, false};
auto val = flags[0]; // 実際には bool ではない
多次元配列へのアクセス
ネストされたコンテナでは,at()
を二重に使うことで,階層ごとに安全なアクセスが可能.
std::vector<std::vector<int>> matrix = {{1, 2}, {3, 4}};
int a = matrix[1][0]; // 危険(未定義動作の可能性)
int b = matrix.at(1).at(0); // 安全なアクセス
例外処理の有無による関数設計の違い
-
at()
はnoexcept
ではないため,例外を許容しない関数では使用に注意. -
noexcept
指定された関数内では使用できないケースもある.
C++20以降の std::span
の活用
-
std::span
は軽量なビュー型で,安全性と柔軟性を兼ね備える. - インデックスアクセスに
at()
のような安全機構を追加できる設計も可能.
まとめ
観点 | [i] |
at(i) |
---|---|---|
安全性 | 低い(未定義動作のリスク) | 高い(例外で保護される) |
パフォーマンス | 高い(範囲チェックなし) | やや低い(範囲チェックあり) |
デバッグ適性 | 低い(バグに気づきにくい) | 高い(例外で検出しやすい) |
推奨場面 | 高速処理・内部処理 | ユーザー入力・教育・バグ予防 |
noexcept | yes | no(例外を投げる可能性がある) |
対応コンテナ | ほとんどのコンテナ | 一部コンテナ(例:list にはなし) |
Summary
-
at(i)
は 安全性が高く例外を投げる. -
[i]
は 高速だが,範囲外アクセスは未定義動作. - 初心者や安全重視の開発では
at()
を使うのが推奨される. - パフォーマンスを優先する場面では
[i]
も選択肢になるが,慎重な設計が求められる.
References
cppreference.com - C++ > Containers library > std::vector
cppreference.com - C++ > Containers library >std::map