C++ Learning

第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)
⭐ 余裕があれば読む
  • autoauto& / const auto& の違い
  • 範囲 for の裏で呼ばれる begin() / end()
  • decltypeauto の違い
  • 構造化束縛 auto [k, v] : map (C++17)

1. auto と範囲 for とは ― まず触ってみる

この回は 2 つの便利機能を一気に覚えます。先に一言ずつ:

まずは体感

従来の書き方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.cppC++ 最小例
#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::vectorstd::arraystd::stringstd::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;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 が必要なければ無駄
範囲 forC++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; }
int など小さい型ならこれで OK
② 参照書き換えたいとき
for (auto& x : v) { // x は要素への参照 x *= 2; // v が書き換わる }
破壊的変更にはこれ
③ const 参照読むだけ・重い型
std::vector<std::string> names = ...; for (const auto& name : names) { std::cout << name << "\n"; } // コピーしない&書き換え禁止の安心感
string/vector/クラスを読むならデフォルト
使い分けまとめ判断フロー
// 書き換える? // 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 0 / 10
Reset 状態。Step を押して 1 段ずつ進めてください。
余裕があれば読む ― ここから先は応用
最低限はここまで(auto/範囲 for の基本と型推論)。残りは罠と C++17 の新機能なので、急ぐなら次章へ。

6. autoauto& の罠

罠 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 : vxbool& ではなくプロキシ型になるので、通常の bool 参照とは振る舞いが違います。「vector<bool> は STL の失敗」と呼ばれる理由の 1 つ。どうしても可変の bool 配列が欲しければ std::vector<char>std::deque<bool> を検討。

7. C++17: 構造化束縛で map を回す

C++17 の構造化束縛 (structured bindings) と組み合わせると、std::pairstd::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 専用ではない: 任意の pairtuple、固定サイズ配列、さらには自作の集約構造体(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; が必要)。
この記事をシェア