Help us understand the problem. What is going on with this article?

range-based for loopsの要素の型について

More than 3 years have passed since last update.

2017/5/6追記
C++17からrange_based for loopsの仕様が一部変わりましたのでコードの一部を加筆・変更しました。該当部分には説明をつけてあります。
また、一部不正確であった部分を修正しました。

はじめに

range-based for loopsとは、つまるところforeachのことです。C++11から、
for( Type elem : container ) { /* dosomething */ }
とすることで書くことができるようになりました。
そして、Typeの部分、すなわち型には、autoを使うことが一般的です。場合によってはauto&や、auto&&などを使い分けることになります。本稿ではその使い分けについて書いていきたいと思います。

auto

std::vector < int > vec;
for( auto elem : vec ) { /* dosomething */ }

単にautoとすると、この場合int型で宣言されたelemに、vecのしかるべきイテレータの実体をコピーという手順が行われます。つまり、おおよそ以下とほぼ同等のコードに置き換えられます。なお、range-based forでは、一時オブジェクト_Rangeやイテレータ_First, _Endを参照することはできません。
なお、この場ではユーザが参照できないことを強調するためにアンダースコア+大文字(_Range,_First,_End)という規格に違反する命名を行っておりますが、実際のコードでは真似しないでください。


C++11-C++14まで

{
auto&& _Range = vec;
using std::begin; using std::end;
for( typename std::vector < int >::iterator _First = begin(_Range),
     _End = end(_Range); _First != _End; ++_First )
{
  int elem = *_First;
  /* dosomething */
}
}

C++17以降

C++17からはrange-based forに対する制限が緩くなりました。具体的には、begin()end()が返すイテレータの型は問わず、_Range::iterator_Range::const_iteratorと等価比較が可能であればよくなりましたので、そのようにコードを改めたものを下に併記しています。

{
auto&& _Range = vec;
using std::begin; using std::end;
auto _First =  begin(_Range);
auto _End = end(_End);
for( ; _First != _End; ++_First )
{
  int elem = *_First;
  /* dosomething */
}
}

ありがたみがよくわかります。しかしここで注意して欲しいのは、上のコードではelemはコピーされているということです。例えば、std::vector < int >std::vector < std::unique_ptr < int > >とするとコンパイルエラーになります。また、コピーに大きなコストがかかるオブジェクトが入ったコンテナを渡すことは控えたいものです。コピーにコストがかからない整数型などはこれが最善のように思えます。

auto&

コピーのコストを気にする場合、しばしば参照が使われます。
for( auto& e : vec ) { /* dosomthing */ }
これなら、ポインタのコピー以上のコストはありません。
また、要素を書き換える場合もこれを使うことになるでしょう。

const修飾をする人もいますが、この場合 e はconst修飾されていないメンバ関数を呼ぶことができなくなります。これはしばしば見つけにくいエラーの原因になります。
しかし、forの前後で要素の一切に影響がないことを保証できます。

for( const auto& e : vec )
{
  e.dosometing(); // dosomething must be a const memberfunction
}

auto&&

なんじゃこりゃと思う方もいらっしゃるかもしれませんが、基本的に、auto&を使うよりはこちらを使うことをおすすめします。というのも、イテレータが右辺値を返す場合、auto&は使えないのです。例えば、悪名高いstd::vector < bool >なんかがそうですね。
基本的に&&は右辺値しか受け取ることができませんが、型推論が絡む場合に限り右辺値は右辺値、左辺値は左辺値へ振り分けてくれる(perfect forwarding)ので、イテレータが左辺値を返そうが右辺値を返そうが正しく動作します。

std::vector < bool >& _Range = vec;
using std::begin; using std::end;
for( typename std::vector < bool >::iterator _First = begin(_Range),
     _End = end(_Range); _First != _End; ++_First )
{
  // bool& elem = *_First; // *_First returns a rvalue!
  auto&& elem = *_First; // ok
  /* dosomething */
}

おわりに

C++17ではTerse Range-Based For Loopsというものが提案されており、それはこの記事を吹き飛ばすような内容です。これがあれば、型指定そのものを省略できます。
for( e : vec ) { /* dosomething */ }
そしてこれは、for( auto&& e : vec ) { /* dosomething */ }としたときと同じになります。

Terse Range-Based For Loopsについては、N3994visual studio 2015 previewのブログなどを御覧ください。(vs2015にTerse Range Based For Loopは含まれません)

2016/5/4 追記
Terse Range-Based For Loops の提案は見送られたようですね。少し残念です。
さて、ありがたいことにこの記事は多くのストックを頂いたのですが、結局どういうシチュエーションでどの型を使えばいいのよということを聞かれたのでそれもまとめとして私個人の考えではありますが載せることとしました。

ポイントは4つです。

  • テンプレート関数の内部など型推論が必須の場合では確実にconst auto&またはauto&&を用いる
  • そのうち、コンテナに変更がない場合はconst auto&
  • コンテナの変更を伴う(非const修飾のメンバ関数の呼び出しを含む)場合はauto&&
  • auto&&またはconst auto&を使わなければならない場面以外では型推論を使っても自分で書いても別にどちらでもいい

まず、上の記事の内容から勘違いしないでいただきたいことは、auto&&は全ての場面で使うことができますが、必ずしも全ての場面でauto&&を使うべきではないということです。逆に、auto&&を使わなければならないという場面もあります。そして、私としてはauto&&またはconst auto&を使わなければならない場面以外では型推論を使っても自分で書いても別にどちらでもいいと思っています。
ただし、forの中でコンテナの変更を伴う(非const修飾のメンバ関数の呼び出しを含む)場合はauto&&、forの前後で一切コンテナに変更がない場合はconst auto&を用いるという使い分けは徹底したほうが良いと思います。

例えばテンプレート関数の内部では確実にauto&&あるいはconst auto&を使うことになります。記事にある理由で、auto&は使う理由がなくなります。実は、auto&はRange-Based For Loops において使う機会は一切ありません。
以下の例で、auto&&またはconst auto&を使わなければならない場面を示します。

template< class Container >
void foo(Container& container)
{
  for (auto&& e : container) { /* containerの中身をいじる処理 */ }   //-- 別にいじらなくてもいいけど読み直すときにダルい
  for (const auto& e : container) { /* containerの中身をいじらない処理 */ }

  for (typename Container::reference e : container) {} // これでもいいけど見にくくなるだけ
  for (typename Container::const_reference e : container) {} // やはり一切のメリットがない
}

それ以外では、可読性の向上の為にちゃんと中身の型を書くことがあります。

void bar(std::vector< int >& vec)
{
  for (const auto& e : vec) { /* いじらない */ } // デリファレンスの分処理速度が落ちる可能性がある。
  for (auto e : container) { /* いじらない */ } // 見やすい。あとで型の要素が変更されたときに強い。
  for (int e : vec) { /* いじらない */ } // 見やすい。

  for (auto&& e : vec) { /* いじる */ } // まあ困らない。あとで型の要素が変更されたときに強い。
  for (int& e : vec) { /* いじる */ } // 見やすい。
}

でも型の名前があんまり長いとautoを使いたくなる。

void baz(std::list< something_library_namespace::something_great_container >& list)
{
  for (const auto& e : list) {} // 見やすい
  for (auto&& e : list) {} // 見やすい

  for (const something_library_namespace::something_great_container& e : list) {} // 長い
  for (something_library_namespace::something_great_container& e : list) {} // 長い
}

大した追記ではないのですが、ご意見などあればコメントを頂けると幸いです。

rinse_
C++初心者
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした