C++の範囲for文について

AI実装検定のご案内

C++の範囲forとは、配列やコンテナなどの要素を、先頭から順番に取り出して処理するための構文です。

正式には「range-based for statement」と呼ばれ、日本語では「範囲for」「範囲ベースfor」「range for」などと呼ばれます。

従来のfor文では、インデックスやイテレータを自分で管理しながらループを書く必要がありました。

一方、範囲forを使うと、「対象の中にある要素を1つずつ処理する」という意図をシンプルに表現できます。

たとえば、配列やvectorの中身をすべて表示したい場合、範囲forを使えば、要素数や添字を意識せずに処理できます。

そのため、コードの見通しがよくなり、添字の指定ミスや範囲外アクセスのようなミスを減らしやすくなります。

目次

範囲forの基本的な考え方

範囲forは、「ある範囲に含まれる要素を1つずつ取り出して処理する」ための仕組みです。

ここでいう「範囲」とは、配列や標準ライブラリのコンテナなど、先頭から末尾まで順番にたどれるものを指します。

代表的なものには、配列、std::vector、std::array、std::string、std::mapなどがあります。

範囲forでは、ループのたびに対象の要素が1つずつ変数に渡され、その変数を使って処理を行います。

通常のfor文のように、何番目の要素かを表すインデックスを自分で用意する必要はありません。

そのため、単純に「すべての要素を順番に処理したい」という場面では、通常のfor文よりも範囲forのほうが自然に書けます。

範囲forで扱えるもの

配列

C++の通常の配列は、範囲forで処理できます。

配列は要素数が分かっているため、範囲forは先頭から最後の要素までを順番に処理できます。

固定された数のデータを順番に扱いたい場合に便利です。

ただし、配列を関数の引数として渡す場合には注意が必要です。

関数の引数に渡された配列は、実際にはポインタとして扱われることが多く、配列の要素数情報が失われます。

そのため、関数内ではそのまま範囲forで扱えない場合があります。

このような場合は、std::arrayやstd::vectorを使うほうが安全です。

std::vector

std::vectorは、範囲forと非常に相性のよいコンテナです。

std::vectorは複数の要素を順番に格納するための標準コンテナであり、範囲forを使えば、中に入っている要素を先頭から順に処理できます。

たとえば、数値の一覧を表示する、合計を求める、すべての値を変更する、といった処理に向いています。

C++で配列のようなデータを扱う場合、通常の配列よりもstd::vectorのほうが柔軟で安全な場面が多いため、範囲forと組み合わせてよく使われます。

std::string

std::stringも範囲forで処理できます。

std::stringは文字の並びとして扱えるため、範囲forを使うと1文字ずつ取り出して処理できます。

たとえば、文字列の各文字を調べたり、特定の文字を数えたり、文字を変換したりする場合に使えます。

ただし、日本語のようなマルチバイト文字を扱う場合は注意が必要です。

std::stringは必ずしも「人間が見る1文字」単位で処理できるわけではありません。

英数字のような単純な文字列であれば扱いやすいですが、日本語文字列を1文字ずつ正しく扱いたい場合には、文字コードやライブラリの知識が必要になることがあります。

std::map

std::mapも範囲forで処理できます。

std::mapは、キーと値をセットで管理するコンテナです。範囲forでstd::mapを処理すると、各要素はキーと値の組として取り出されます。

たとえば、名前と点数、商品名と価格、IDとデータのような対応関係を順番に処理したい場合に便利です。

C++17以降では、構造化束縛を使うことで、キーと値に分けて分かりやすく扱えます。

これにより、従来のように「first」や「second」といった名前を使うよりも、意味のある変数名で処理を書きやすくなります。

autoを使う範囲for

autoとは

範囲forでは、要素を受け取る変数の型にautoを使うことがよくあります。

autoは、変数の型をコンパイラに推論させるための機能です。

たとえば、コンテナの要素が整数なら整数型として、文字列なら文字列型として、自動的に型が決まります。

範囲forではコンテナの要素型が明確なことが多いため、autoを使うことでコードを簡潔にできます。

特に、std::mapのように要素型が長くなりやすいコンテナでは、autoを使うことで可読性が上がります。

autoを使うメリット

autoを使うメリットは、型名を短く書けることです。

C++では、コンテナの型が複雑になることがあります。

たとえば、mapの要素や、vectorの中に構造体を入れたものなどは、正確な型名を書くと長くなりがちです。

そのような場合でも、autoを使えば、型を細かく書かずに要素を受け取れます。

ただし、autoを使うときには注意も必要です。

autoだけで受け取るのか、auto&で受け取るのか、const auto&で受け取るのかによって、コピーされるのか、元の要素を参照するのか、変更できるのかが変わります。

auto、auto&、const auto&の違い

autoはコピーして受け取る

範囲forでautoだけを使うと、各要素はコピーされて変数に渡されます。

つまり、ループ内でその変数の値を変更しても、元のコンテナの要素は変更されません。

これは、数値や文字のような軽い値を読むだけなら問題ありません。

しかし、std::stringや大きな構造体のようにコピーコストがある要素では、毎回コピーが発生するため、無駄が大きくなることがあります。

また、「要素を変更したつもりなのに元のデータが変わっていない」という初心者によくあるミスも、autoによるコピーが原因で起こります。

auto&は参照して受け取る

auto&を使うと、各要素を参照として受け取ります。

参照として受け取るということは、ループ内の変数が元の要素そのものを指すということです。

そのため、ループ内で値を変更すると、元のコンテナの要素も変更されます。

すべての数値を2倍にしたい、文字列の一部を変更したい、構造体のメンバを更新したい、といった場面ではauto&を使います。

ただし、変更する必要がない場面でauto&を使うと、うっかり元のデータを書き換えてしまう可能性があります。

読み取り専用であれば、const auto&を使うほうが安全です。

const auto&は読み取り専用の参照

const auto&は、要素を参照として受け取りつつ、変更できないようにする書き方です。

コピーを避けられるため効率的であり、さらにconstによって誤って値を書き換えることも防げます。

実務では、コンテナの中身を表示するだけ、集計するだけ、条件を調べるだけといった読み取り専用の処理では、const auto&がよく使われます。

特に、std::string、構造体、クラス、vectorの中に入った大きなオブジェクトなどを扱う場合は、const auto&を使うことで不要なコピーを避けられます。

範囲forでよく使う受け取り方

小さな値を読むだけならautoでもよい

整数、文字、浮動小数点数などの小さな値を読むだけなら、autoでコピーしても大きな問題にはなりにくいです。

たとえば、数値の合計を求めるだけであれば、要素をコピーして処理しても十分です。

ただし、後から型が大きなオブジェクトに変わる可能性がある場合や、最初から読み取り専用であることを明確にしたい場合は、const auto&を使っておくと安全です。

元の要素を変更したいならauto&を使う

コンテナの中身を直接変更したい場合は、auto&を使います。

autoだけではコピーを変更しているだけなので、元の要素は変わりません。

これは非常によくある間違いです。

要素の値を更新する、文字列を書き換える、構造体のメンバを変更する、といった処理では、参照で受け取る必要があります。

読むだけでコピーを避けたいならconst auto&を使う

読み取り専用で、かつコピーを避けたい場合はconst auto&を使います。

実務では、この書き方が非常によく使われます。

なぜなら、安全性と効率のバランスがよいからです。

constを付けることで「このループでは要素を変更しない」という意図も明確になります。

コードを読む人にとっても、処理の目的が分かりやすくなります。

auto&&を使う場面

auto&&は汎用的な受け取り方

auto&&は、より汎用的に要素を受け取りたい場合に使われます。

通常の学習段階では、auto、auto&、const auto&を理解していれば十分です。

ただし、テンプレートや汎用的な処理を書く場合、あるいは特殊なコンテナを扱う場合にはauto&&が役立つことがあります。

auto&&は、対象の要素の性質に応じて適切に型が推論されます。

そのため、汎用的なコードでは便利な受け取り方です。

std::vectorでは注意が必要

std::vectorは、通常のstd::vectorとは少し異なる特殊なコンテナです。

通常のvectorでは、要素を参照すると本物の要素への参照が得られます。

しかしstd::vectorでは、要素がビット単位で詰めて管理されることがあり、通常のbool&として扱えない場合があります。

そのため、std::vectorを範囲forで扱うと、auto&では直感と違う挙動になったり、扱いにくくなったりすることがあります。

std::vectorの要素を書き換える場合は、auto&&を使うほうが適していることがあります。

また、用途によってはstd::vector、std::vector、std::bitsetなどを使うことも検討できます。

範囲forとstd::map

mapの要素はキーと値の組

std::mapの各要素は、キーと値の組です。

範囲forでstd::mapを処理すると、1回のループごとに1つのキーと値の組を受け取ります。

キーを使って分類したり、値を表示・集計したりする処理に向いています。

たとえば、ユーザー名と年齢、商品名と価格、単語と出現回数などを管理している場合、範囲forで順番に取り出せます。

mapのキーは変更できない

std::mapでは、キーを直接変更することはできません。

なぜなら、std::mapはキーの順序を使って内部的に要素を管理しているためです。

もしキーを自由に変更できてしまうと、mapの並び順や検索の仕組みが壊れてしまう可能性があります。

一方、値のほうは変更できます。

そのため、mapを範囲forで処理するときは、「キーは読み取り専用」「値は必要に応じて変更可能」と理解しておくとよいです。

C++17以降は構造化束縛が便利

C++17以降では、構造化束縛を使うことで、mapのキーと値を分かりやすく受け取れます。

従来は、キーと値を取り出すためにfirstやsecondを使うことが多くありました。

しかし、firstやsecondという名前だけでは、それが何を意味しているのか分かりにくい場合があります。

構造化束縛を使えば、キーにはkey、名前ならname、点数ならscoreのように、意味のある名前を付けられます。

その結果、コードの可読性が大きく向上します。

範囲forと配列の注意点

配列そのものには使える

C++の配列そのものには、範囲forを使えます。

配列の要素数が分かっている状態であれば、範囲forはその範囲を正しく処理できます。

固定長の小さなデータを扱う場合には、通常の配列でも範囲forは便利です。

関数に渡すと配列情報が失われる場合がある

注意したいのは、配列を関数に渡した場合です。

関数の引数に配列を書いても、実際にはポインタとして扱われることがあります。

ポインタには「要素がいくつあるか」という情報が含まれていません。

そのため、関数内で範囲forを使おうとしても、どこからどこまでを処理すればよいのか分からず、うまく扱えない場合があります。

この問題を避けるには、std::arrayやstd::vectorを使うのが一般的です。

固定長ならstd::arrayも便利

固定長の配列を安全に扱いたい場合は、std::arrayが便利です。

std::arrayは、通常の配列に近い使い方ができる一方で、標準コンテナとしての性質も持っています。

範囲forでも扱いやすく、関数に渡してもサイズ情報を保ちやすいというメリットがあります。

固定長であることが明確なデータにはstd::array、要素数が変化するデータにはstd::vectorを使う、というように使い分けるとよいでしょう。

範囲forの内部的な仕組み

beginとendを使って範囲をたどる

範囲forは、内部的にはbeginとendに相当するものを使って範囲をたどります。

beginは範囲の先頭を表し、endは範囲の終わりを表します。

ただし、endは最後の要素そのものではなく、最後の次の位置を表すものです。

範囲forは、beginから順番に要素をたどり、endに到達するまで処理を続けます。

この仕組みにより、vector、array、string、mapなど、さまざまなコンテナを同じような感覚で処理できます。

メンバ関数だけでなく非メンバのbegin/endが使われる場合もある

範囲forでは、対象が配列であれば配列の範囲が使われます。

対象がクラス型の場合は、メンバ関数としてbeginとendが見つかれば、それらが使われます。

それ以外の場合には、非メンバ関数のbeginとendが探される場合もあります。

つまり、「beginとendが使えるもの」と説明するのは大きくは正しいですが、厳密にはメンバ関数だけに限定されるわけではありません。

初心者の段階では、標準コンテナや配列は範囲forで扱える、と理解しておけば十分です。

C++17以降ではbeginとendの型が異なってもよい場合がある

C++17以降では、範囲forにおいてbegin側とend側の型が必ずしも同じである必要はありません。

比較できる形になっていれば、異なる型でも範囲として扱える場合があります。

これは少し高度な話なので、通常のvectorやstringを使う範囲では意識しなくても問題ありません。

ただし、範囲forの仕組みを厳密に理解したい場合には、beginとendの型が必ず同じとは限らない点も知っておくとよいです。

constなコンテナを範囲forで回す場合

constなコンテナの要素は変更できない

コンテナ自体がconstの場合、範囲forで取り出した要素も変更できません。

たとえば、読み取り専用として渡されたvectorの中身を範囲forで処理する場合、各要素は変更できないものとして扱われます。

このとき、明示的にconst auto&で受け取ると、読み取り専用であることが分かりやすくなります。

読み取り専用ならconst auto&が自然

関数の引数としてコンテナを受け取り、その中身を表示するだけ、集計するだけ、条件判定するだけであれば、const auto&を使うのが自然です。

この書き方は、コピーを避けられるうえに、誤って値を書き換えることも防げます。

特に、複数人でコードを書く場合や、あとで自分が読み返す場合にも、「このループでは要素を変更しない」という意図が伝わりやすくなります。

範囲forとインデックス

インデックスが不要な処理に向いている

範囲forは、インデックスを使わずに要素を順番に処理したい場合に向いています。

たとえば、すべての要素を表示する、合計を求める、条件に合う要素を探す、すべての要素を変更する、といった処理では、範囲forが便利です。

インデックスを自分で管理しないため、コードが短くなり、添字のミスも減らせます。

インデックスが必要なら通常のfor文も選択肢

一方で、何番目の要素かが重要な処理では、通常のfor文のほうが分かりやすい場合があります。

たとえば、要素番号と一緒に表示したい場合、前後の要素と比較したい場合、特定の位置だけ処理したい場合などです。

範囲forでも、別にカウンタ変数を用意すればインデックスのようなものを扱えます。

しかし、その場合は通常のfor文のほうが自然なことも多いです。

範囲forは便利ですが、常に最適というわけではありません。

処理の目的に応じて、通常のfor文と使い分けることが大切です。

範囲forとコンテナの変更

要素の値を変更するのは問題ない

範囲forでauto&を使えば、コンテナ内の要素そのものを変更できます。

たとえば、すべての数値を増やす、文字列を変更する、構造体のメンバを書き換える、といった処理は問題ありません。

この場合、変更しているのはコンテナの構造ではなく、既に存在している要素の値です。

ループ中に要素を追加・削除するのは注意が必要

一方で、範囲forで処理している最中に、同じコンテナへ要素を追加したり、削除したりするのは注意が必要です。

特にstd::vectorでは、要素を追加したときに内部のメモリが再確保される場合があります。

そうなると、ループで使っている内部的な位置情報が無効になり、正しく動作しなくなる可能性があります。

また、要素を削除する場合も、ループ中の位置情報がずれてしまうことがあります。

そのため、範囲for中に同じコンテナの構造を変更する処理は、基本的には避けるのが安全です。

追加・削除したい場合は別の方法を使う

ループ中に要素を追加したい場合は、追加分を別のコンテナに一度保存して、ループが終わってからまとめて追加する方法があります。

削除したい場合は、範囲forではなく、適切なアルゴリズムやイテレータを使った処理にするほうが安全です。

C++20以降であれば、条件に合う要素を削除するための便利な標準機能もあります。

C++20より前でも、標準アルゴリズムを組み合わせることで安全に削除できます。

範囲forと一時オブジェクト

関数が返したコンテナをそのまま回すことはできる

範囲forでは、関数が返したコンテナをそのまま処理することもできます。

たとえば、関数がvectorを返し、その戻り値を直接範囲forで回すような使い方は可能です。

この場合、返された一時的なコンテナの寿命はループの終わりまで延長されるため、問題なく処理できます。

ちょっとしたデータ生成や、関数の結果をそのまま処理したい場合に便利です。

一時オブジェクトの内部範囲を回す場合は注意

ただし、一時オブジェクトから内部の範囲を取り出して、それを範囲forで回すような場合には注意が必要です。

たとえば、関数が一時的なオブジェクトを返し、そのオブジェクトの内部データへの参照をさらに取得して範囲forで回すような形では、C++のバージョンや書き方によっては危険なケースがあります。

安全性を重視するなら、一時オブジェクトをいったん変数に受けてから、その変数に対して範囲forを使うとよいです。

C++20以降では、範囲forの中に初期化文を書けるため、ループ専用の一時変数を用意しやすくなっています。

C++20以降の範囲for

初期化付き範囲forが使える

C++20以降では、範囲forに初期化文を書けます。

これにより、ループのためだけに使う変数を、範囲forの中で用意できます。

一時オブジェクトを安全に扱いたい場合や、ループの外に余計な変数を残したくない場合に便利です。

ただし、初心者のうちは必ず使う必要はありません。まずは基本的な範囲forに慣れることが大切です。

std::viewsと組み合わせられる

C++20以降では、Rangesライブラリのstd::viewsと範囲forを組み合わせることもできます。

たとえば、条件に合う要素だけを取り出して処理したり、変換しながら処理したりすることができます。

これは非常に便利ですが、通常の範囲forよりも少し高度な内容です。

まずは基本的な範囲forを理解し、その後でstd::viewsやRangesを学ぶとよいでしょう。

範囲forが向いている場面

すべての要素を順番に処理したい場合

範囲forが最も向いているのは、コンテナ内のすべての要素を順番に処理したい場合です。

要素を表示する、合計を求める、条件を調べる、各要素に同じ処理を行う、といった場面では非常に便利です。

インデックスやイテレータを自分で管理する必要がないため、処理の意図が分かりやすくなります。

要素の位置よりも値が重要な場合

範囲forは、「何番目の要素か」よりも「要素の値そのもの」が重要な処理に向いています。

たとえば、点数の一覧から合計を求める場合、通常は何番目の点数かよりも、それぞれの点数の値が重要です。

このような処理では、範囲forを使うことで簡潔に書けます。

コンテナの全要素を変更したい場合

範囲forは、コンテナ内の全要素をまとめて変更したい場合にも便利です。

ただし、その場合はauto&で要素を参照として受け取る必要があります。

autoだけで受け取るとコピーを変更するだけになり、元のコンテナの要素は変わりません。

範囲forが向いていない場面

インデックスが重要な場合

要素の位置が重要な処理では、通常のfor文のほうが向いている場合があります。

たとえば、隣り合う要素を比較したい場合、偶数番目だけ処理したい場合、インデックスを表示したい場合などです。

範囲forでも工夫すれば対応できますが、無理に範囲forを使うよりも、通常のfor文を使ったほうが読みやすいことがあります。

逆順に処理したい場合

範囲forは基本的に先頭から末尾へ順番に処理する用途に向いています。

逆順に処理したい場合は、逆イテレータやC++20以降のviewsを使う方法があります。

ただし、初心者向けには通常のfor文や逆イテレータを使うほうが理解しやすい場合もあります。

ループ中に要素を追加・削除したい場合

ループ中に同じコンテナへ要素を追加したり、削除したりする処理は、範囲forにはあまり向いていません。

特にstd::vectorでは、追加や削除によって内部的な位置情報が無効になる可能性があります。

このような場合は、範囲forではなく、イテレータを明示的に使う方法や、標準アルゴリズムを使う方法を検討するべきです。

範囲forでよくあるミス

autoで受け取って元の要素を変更したつもりになる

最もよくあるミスは、autoで受け取っているのに、元の要素を変更したつもりになってしまうことです。

autoだけを使うと、要素はコピーされます。そのため、ループ内で値を変更しても、元のコンテナには反映されません。

元の要素を変更したい場合は、auto&を使う必要があります。

大きなオブジェクトを毎回コピーしてしまう

std::stringや構造体、クラスなどをautoで受け取ると、毎回コピーが発生します。

小さなデータであれば大きな問題にならないこともありますが、大きなオブジェクトでは無駄な処理になります。

読み取り専用であれば、const auto&を使ってコピーを避けるのが基本です。

constを付けずに意図せず変更できる状態にしてしまう

読み取りだけの処理なのにauto&を使うと、要素を変更できる状態になります。

そのため、誤って値を書き換えてしまう可能性があります。

読み取り専用の処理であれば、const auto&を使うことで、安全性と可読性を高められます。

ループ中にコンテナの構造を変えてしまう

範囲forで回している最中に、同じコンテナへ要素を追加したり削除したりすると、問題が起きる可能性があります。

特にstd::vectorでは、内部メモリの再確保によって、ループの処理が不安定になることがあります。

要素の値を変えるのは問題ありませんが、コンテナの構造を変える処理には注意が必要です。

範囲forのベストプラクティス

読み取り専用ならconst auto&を基本にする

読み取り専用の処理では、const auto&を基本にするとよいです。

コピーを避けられるため効率的で、さらに要素を変更しないことも明確になります。

特に、文字列、構造体、クラスなどを扱う場合は、const auto&が実務でもよく使われます。

変更したい場合はauto&を使う

コンテナの要素を変更したい場合は、auto&を使います。

autoではコピーになるため、元の要素は変わりません。

「元のコンテナを書き換えたいのか」「一時的なコピーを処理したいだけなのか」を意識して使い分けることが大切です。

小さな値ならautoでもよい

整数や文字などの小さな値を読むだけであれば、autoでコピーしても問題になりにくいです。

ただし、迷った場合や要素型が大きくなる可能性がある場合は、const auto&を使うほうが安全です。

インデックスが必要なら通常のfor文を使う

範囲forは便利ですが、インデックスが必要な処理には向いていない場合があります。

要素番号が重要な処理では、通常のfor文を使うほうが分かりやすくなります。

C++では、範囲forと通常のfor文を目的に応じて使い分けることが重要です。

追加・削除を伴う処理では慎重に使う

範囲forで回しているコンテナに対して、同時に要素の追加や削除を行うのは避けたほうが安全です。

追加や削除が必要な場合は、別のコンテナを使う、標準アルゴリズムを使う、イテレータを明示的に管理するなど、別の方法を検討しましょう。

初心者がまず覚えるべきポイント

autoはコピー

autoだけで受け取ると、要素はコピーされます。

そのため、元の要素を変更したい場面では使えません。

小さな値を読み取るだけなら問題ありませんが、大きなオブジェクトではコピーコストに注意が必要です。

auto&は変更用

auto&は、元の要素を直接参照します。

ループ内で値を変更すれば、コンテナの中身も変更されます。

すべての要素を更新したい場合にはauto&を使います。

const auto&は読み取り用

const auto&は、元の要素をコピーせずに参照し、さらに変更できないようにする書き方です。

読み取り専用の処理では、この書き方が安全で効率的です。

実務でも非常によく使われます。

範囲forは万能ではない

範囲forは便利ですが、すべてのループ処理に最適というわけではありません。

インデックスが必要な場合、逆順に処理したい場合、ループ中にコンテナの構造を変更したい場合などは、通常のfor文やイテレータ、標準アルゴリズムを使うほうが適しています。

まとめ

C++の範囲forは、配列やコンテナの要素を順番に処理するための便利な構文です。

通常のfor文に比べて、インデックスやイテレータを自分で管理する必要がないため、コードを簡潔に書けます。

特に、すべての要素を順番に処理したい場合には非常に有効です。

範囲forを使ううえで最も重要なのは、要素の受け取り方です。

autoはコピー、auto&は参照、const auto&は読み取り専用の参照です。

元の要素を変更したい場合はauto&を使い、読み取りだけでコピーを避けたい場合はconst auto&を使います。

小さな値を読むだけならautoでも問題ありません。

また、std::mapではキーと値の組を扱うこと、std::vectorは特殊なコンテナであること、ループ中にコンテナへ要素を追加・削除する処理には注意が必要であることも覚えておきましょう。

範囲forは、C++のコードを読みやすく、安全に書くための重要な機能です。

まずは基本の使い方を押さえ、処理の目的に応じて通常のfor文や標準アルゴリズムと使い分けることが大切です。

以上、C++の範囲for文についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次