C言語 › C++ › 第5回 auto と範囲 for
第5回 auto と範囲 for
C++11 で入った 2 つの目玉機能。auto は「コンパイラに型を推論させる」、範囲 for は「コンテナを 1 行でなめる」。C の for(int i=0; i<n; i++) と型名フルスペルの呪縛 から解放されます。本回は型推論プレイグラウンド と範囲 for の展開アニメ で、何が裏で起きているかを覗きます。
このページで押さえること
✅ 最低限ここだけ覚える
auto x = ...; で型は右辺から自動推論
for (auto x : v) で全要素を順番に訪問
書き換えるなら for (auto& x : v)
読むだけなら for (const auto& x : v)
⭐ 余裕があれば読む
auto と auto& / const auto& の違い
範囲 for の裏で呼ばれる begin() / end()
decltype と auto の違い
構造化束縛 auto [k, v] : map (C++17)
目次
1. auto と範囲 for とは ― まず触ってみる
2. auto とは ― 型をコンパイラに任せる
3. 型推論プレイグラウンド
4. 範囲 for の基本 3 パターン
5. 範囲 for の展開アニメ
6. auto と auto& の罠
7. C++17: 構造化束縛で map を回す
1. auto と範囲 for とは ― まず触ってみる
この回は 2 つの便利機能 を一気に覚えます。先に一言ずつ:
auto = 「型を書く代わりに書くキーワード」。= の右を見て、コンパイラが型を勝手に決めてくれる。
範囲 for = 「配列・vector の全要素を順番に取り出す for」。C の for(int i=0; i<n; i++) を短くしたもの。
まずは体感
従来の書き方 C っぽい
std::vector <int > v = {10 , 20 , 30 };
// 型を明示、インデックスで回す
for (int i = 0 ; i < v.size (); i++) {
int x = v[i];
std::cout << x << "\n" ;
}
auto + 範囲 for モダン C++
auto v = std::vector <int >{10 , 20 , 30 };
// 型は auto、要素は直接取り出す
for (auto x : v) {
std::cout << x << "\n" ;
}
どちらも同じ動作 (10, 20, 30 を改行区切りで出力)。右のほうが「何をしたいか」だけ書いている ので読みやすいことに注目してください。
🎯 範囲 for が 1 つずつ値を取り出す様子
▶ 次の要素
↺ リセット
まずは 4 行だけ
first_auto.cpp C++ 最小例
#include <vector>
#include <iostream>
int main () {
std::vector <int > v = {1 , 2 , 3 };
for (auto x : v) { // ← これだけ
std::cout << x << " " ;
}
// 出力: 1 2 3
}
ここまでで覚えること(2 つだけ):
auto 名前 = 式; で型を自動推論(型を書かなくていい)
for (auto x : v) { ... } で v の全要素を順番に処理
よくある素朴な疑問
Q. auto って JavaScript の var や Python の変数みたいなもの?
→ 違います。 auto はコンパイル時 に型が決まって、その後は変わりません。auto x = 42; と書いたら x は int に決まり、あとで x = "hello"; としたらエラー。「書くのが省略できるだけ」で、動的型付けではありません。
Q. 範囲 for はどんなものに使える?
→ std::vector、std::array、std::string、std::map、C の生配列など、「最初から最後まで順にたどれる」ものなら何でも 。
Q. インデックス i が欲しいときは?
→ 範囲 for では i を取れません。インデックスが必要なら従来の for(int i=0; i<v.size(); ++i) に戻すか、auto& x : v と別に自分でカウンタを持つ。
次のセクション以降は少し細かい話になります。 「なぜ auto は便利なのか」「auto& と auto の違い」「map を回すときの C++17 技」など。最初の §2〜§3 を読み終えれば基本は押さえたことになるので、急ぐならそこまでで OK。
2. auto とは ― 型をコンパイラに任せる
auto は「右辺の式から型を自動で決めてください」というキーワードです。C++11 で導入されました(C には無い機能)。
型を明示 冗長
std::vector <std::pair <int , std::string >> v;
std::vector <std::pair <int , std::string >>::iterator
it = v.begin ();
// 型名だけで1行!
auto で任せる C++11~
std::vector <std::pair <int , std::string >> v;
auto it = v.begin ();
// 推論される型は上と同じ
勘違いされがちですが: auto は静的型付け です。型推論はコンパイル時 に決まり、実行時に変わることはありません。JavaScript の var や Python の変数のような動的型付け とは全く違います。
そもそも「正しく型を書く」とどれだけ長いか
「auto って単なる手抜き?」と感じる方もいるかもしれません。でも実は、STL の型名はネストが深くなるほど爆発的に長くなる のが現実です。auto を使わずに「本来の型名をフルで書く」とどうなるか、3 段階で体感してみましょう。
① 軽い例 ― vector<int> の反復子
vector<int> の begin() auto なし vs あり
// auto なし(型をフルで書く)
std::vector <int >::iterator it = v.begin ();
// ↑ 型名だけで 26 文字。まだ読める
// auto あり(同じ型に推論される)
auto it = v.begin ();
② きつい例 ― ネストした map の const_iterator
map<string, vector<int>> を読む auto なし vs あり
// auto なし(型をフルで書く)
std::map <std::string , std::vector <int >>::const_iterator
it = m.cbegin ();
// ↑ 型名だけで 55 文字。1 行に収まらず折り返し
// auto あり
auto it = m.cbegin ();
もう「書く気が失せる」レベル。しかもネストが一段深くなるたびに <...> の中身がさらに膨らむので、実務コードでは 80 文字を余裕で超える型名 も珍しくありません。変更に弱い(中身の型が 1 つ変わると全部の型名を書き直す必要がある)のも難点です。
③ 極端な例 ― ラムダは「型名が書けない」
ラムダ式 そもそも書けない
// このラムダの型は…?
auto f = [](int x) { return x * 2 ; };
// ラムダの型はコンパイラが作る "匿名クラス" なので、
// プログラマには名前が分からない = 書きようがない。
// auto(または decltype)を使うしかない。
ラムダの型は「コンパイラが毎回勝手に作る無名の型」なので、人間には型名を書く方法が存在しません 。std::function<int(int)> で受けることはできますが、それは「ラムダを別の型にコピー/ラップ」しているだけで、元の型そのものではありません。
まとめ: auto は「書くのを省略する飾り」ではなく、ネストしたテンプレートやラムダでは正しい型を書くこと自体が困難/不可能 だからこそ用意された、モダン C++ の必須ツールです。ここから先は auto を前提にコードを読み書きしていきます。
C にもあった auto?
実は C にも auto キーワードはありましたが、意味は「自動記憶域 (スタック)」で、ローカル変数につく既定の記憶域クラス。誰も書かない死語でした。C++11 はそれを型推論の意味に再利用 した形です。
3. 型推論プレイグラウンド
右辺のパターンを選ぶと、auto x = ...; でどんな型が推論されるかを表示します。
▶ auto 型推論デモ
auto x =
42
42u (unsigned)
42L
3.14
3.14f (float)
true
'A'
"hello"
std::string("hello")
std::vector<int>{1,2,3}
v.begin() (v は vector<int>)
v[0] (int& のはずが…)
new int(42)
[](int x){ return x*2; }
推論ルール早見表
auto x = 42; → int
auto x = 3.14; → double
auto x = 3.14f; → float
auto x = "hello"; → const char*
※ std::string ではない! 文字列リテラルは C の char 配列 → ポインタに減衰
auto x = std::string("hi"); → std::string
auto x = v[0]; // v は vector<int> → int
※ int& ではない! auto は参照や const を落とす (コピーになる)
auto& x = v[0]; → int&
※ 参照を保持したいなら明示的に & を付ける
const auto& x = v[0]; → const int&
※ 読むだけならこれ ― コピーせず変更もできない
重要な落とし穴: auto は参照と const を取り除いた型 を推論します。auto x = v[0] は「v の中身をコピーした int」になります。書き換えたいのに変わらない!と悩む人が続出するポイントです。
4. 範囲 for の基本 3 パターン
用途に応じて、参照修飾を使い分けます。C の for(int i=0; i<n; i++) から解放される最もよく使う機能です。
C スタイル 従来
std::vector <int > v = {1 ,2 ,3 };
for (int i = 0 ; i < v.size (); ++i) {
std::cout << v[i] << "\n" ;
}
// 冗長。i が必要なければ無駄
範囲 for C++11~
std::vector <int > v = {1 ,2 ,3 };
for (auto x : v) {
std::cout << x << "\n" ;
}
// 要素の列を直接書く
3 パターンを使い分ける
① 値コピー 読むだけ・安い型
for (auto x : v) {
// x は要素のコピー
// 書き換えても v は変わらない
x *= 2 ;
}
② 参照 書き換えたいとき
for (auto & x : v) {
// x は要素への参照
x *= 2 ; // v が書き換わる
}
③ const 参照 読むだけ・重い型
std::vector <std::string > names = ...;
for (const auto & name : names) {
std::cout << name << "\n" ;
}
// コピーしない&書き換え禁止の安心感
使い分けまとめ 判断フロー
// 書き換える?
// YES → for (auto& x : v)
// NO → 要素が重い?
// YES → for (const auto& x : v)
// NO → for (auto x : v)
5. 範囲 for の展開アニメ
範囲 for 文は、コンパイラがイテレータを使った普通のループに展開 しているだけです。動きを見れば仕組みが分かります。下の「Step」で 1 段ずつ進めてみてください。
あなたが書くコード
std::vector <int > v = {10 ,20 ,30 };
for (auto & x : v) {
x *= 2 ;
}
コンパイラが生成するコード (概念)
auto && __r = v; // ← range ref
auto __it = __r.begin (); // ← iterator
auto __end = __r.end ();
for (; __it != __end; ++__it) {
int & x = *__it; // ← 参照取得
x *= 2 ;
}
▶ Step
⏩ Run All
↺ Reset
step 0 / 10
Reset 状態。Step を押して 1 段ずつ進めてください。
⭐
余裕があれば読む ― ここから先は応用
最低限はここまで(auto/範囲 for の基本と型推論)。残りは罠と C++17 の新機能なので、急ぐなら次章へ。
6. auto と auto& の罠
罠 1: 書き換えたつもりが反映されない
NG コピーを編集
std::vector <int > v = {1 ,2 ,3 };
for (auto x : v) {
x *= 10 ; // x は v[i] のコピー
}
// v は {1, 2, 3} のまま!
OK 参照で書き換え
std::vector <int > v = {1 ,2 ,3 };
for (auto & x : v) {
x *= 10 ;
}
// v は {10, 20, 30}
罠 2: 大きな要素をコピーしてしまう
遅い 毎回コピー
std::vector <std::string > names = ...;
for (auto name : names) {
// 毎回 string のコピーが起きる
std::cout << name << "\n" ;
}
速い const 参照
std::vector <std::string > names = ...;
for (const auto & name : names) {
std::cout << name << "\n" ;
}
// コピー 0 回
罠 3: 範囲 for 中にコンテナを変更しない
ループの中 で push_back / erase を呼ぶと、裏で動いているイテレータが無効になり未定義動作。変更が必要なら古典的な for (size_t i = 0; i < v.size(); ++i) に戻すか、erase-remove イディオム(第 9 章のアルゴリズム特集で扱う)を使います。
罠 4: bool の vector<bool>
std::vector<bool> はビットパック特殊化 されていて、各要素が bool ではなく 1 ビットで格納されます。auto& x : v の x は bool& ではなくプロキシ型 になるので、通常の bool 参照とは振る舞いが違います。「vector<bool> は STL の失敗」と呼ばれる理由の 1 つ。どうしても可変の bool 配列が欲しければ std::vector<char> や std::deque<bool> を検討。
7. C++17: 構造化束縛で map を回す
C++17 の構造化束縛 (structured bindings) と組み合わせると、std::pair や std::map の要素を分解しながら回せます。
C++11 時代 .first/.second
std::map <std::string , int > scores = ...;
for (const auto & p : scores) {
std::cout << p.first
<< ": "
<< p.second << "\n" ;
}
C++17 構造化束縛
std::map <std::string , int > scores = ...;
for (const auto & [name, score] : scores) {
std::cout << name
<< ": "
<< score << "\n" ;
}
構造化束縛は map 専用ではない: 任意の pair、tuple、固定サイズ配列、さらには自作の集約構造体(public メンバだけの struct)でも使えます。関数から複数の値を返すとき、auto [a, b] = func(); と受け取れるのも嬉しい。詳しくは第 12 回(pair/tuple)で扱います。
確認クイズ
auto と範囲 for の理解度を 4 問で確認しましょう。
Q1. auto x = 3.14; のとき、x の型は?
float
double
long double
実行時まで不明(動的型)
C++ の浮動小数点リテラルは既定で double 型。float にしたければ 3.14f、long double なら 3.14L のようにサフィックスを付けます。auto はコンパイル時 に型を決める静的型推論です。
Q2. 次のコードを実行したあと、v の中身は?std::vector<int> v = {1,2,3}; for (auto x : v) x *= 10;
{10, 20, 30}
{1, 2, 3}
コンパイルエラー
{10, 2, 3}
auto x は要素のコピー になるので、x を書き換えても v は変わりません。要素自体を書き換えたいときは auto& x にします。この罠は「範囲 for で値が更新されない」あるあるの王様。
Q3. std::vector<std::string> の要素を読み取りだけで回すとき、最も良い書き方は?
for (auto s : v)
for (auto& s : v)
for (std::string s : v)
for (const auto& s : v)
string は SSO を超えるとヒープを伴うのでコピーが重い。const auto& ならコピーゼロ で、かつ const で不用意な変更も防げる。これがモダン C++ の既定の書き方です。
Q4. auto x = "hello"; としたとき x の型は?
std::string
char[6]
const char*
auto
C++ の文字列リテラルは C と同じく const char[N]。値初期化で auto に渡すと、配列→ポインタへの配列→ポインタ減衰 が起きて const char* になります。std::string にしたければ auto x = std::string("hello"); または C++14 のサフィックス auto x = "hello"s;(using namespace std::literals; が必要)。