読者です 読者をやめる 読者になる 読者になる

Range based forとauto&&によるperfect forwarding

C++

C++11からrange based forという構文が導入された。これはpythonなどで言うところのforeach文と似たような動作をする構文である。以下に典型的な使用例を示す。

std::vector<int> vec(10);

/* vecに何か値を設定 */

for(int el : vec)
{
  /* Do something using "el". */
}


ここで、elはコンテナの要素のコピーである。コンテナに格納するオブジェクトが大きく、コピーのコストが気になる場合や、ループ内でコンテナの要素を更新したい場合には参照を使うこともできる。

std::vector<MyObject> vec(10); // vectorのテンプレート引数にユーザ定義型を指定

/* vecの中身をいろいろ更新 */

for(MyObject& el : vec) // ループ変数を参照型にできる
{
  /* Do something using "el". */
}

さらなる工夫として、ループ変数の型をautoとし、型推論させることも可能である。

for(auto& el : vec) // autoによる型推論
{
  /* Do something using "el". */
}

上記のように、autoに対して参照を使うことも可能である。その他、コピーのコストが気になる場合で、かつコンテナの要素が変更されないことを保証したければ、ループ変数にconstを指定することもできる。

for(const auto& el : vec) // const + 型推論
{
  /* Do something using "el". */
}

さて、ここからが本題である。今、vecは変数として宣言しているため、当然ながら左辺値となる。そのため、ループ変数の型は当然左辺値参照(auto&)となる。

では、ループ対象のコンテナが右辺値の場合はどうなるだろうか?この場合、ループ変数を右辺値参照型で定義しなければならないのだろうか?答えはYesである。典型的にはstd::vectorに対してループを回す例が挙げられる。

for(auto&& el : std::vector<bool>(10)) // std::vector<bool>は右辺値を返す。
{
  /* Do something using "el". */
}

実際、このコードは正しく動く。ただし、通常の右辺値参照型と異なり、auto&&という型は特別な意味を持つ。それがすなわちperfect forwardingである。perfect forwardingとは、右辺値参照は右辺値参照に、左辺値参照は左辺値参照にといった具合に、適切に型を判断して振る舞ってくれる型のことである。auto&&の他に、this&&も同じくperfect forwardingを行うらしいが、こちらは詳しく知らない。

このような背景から、range based forのループ変数に対しては、常にauto&&としておくのがよいというような記事をネット上でよく見かける。auto&&を使っておけば、ループ対象コンテナが右辺値か左辺値かなどといちいち気にしなくて良い、というロジックである。(constを使用したい場合は別だが。)

ちなみに、auto&&の代わりにauto&を使用すると本当にダメなのか試してみたところ、以下のようなコンパイルエラーが出た。

error: invalid initialization of non-const reference of type ‘std::_Bit_reference&’ from an rvalue of type ‘std::_Bit_iterator::reference {aka std::_Bit_reference}’
  for(auto& el : std::vector<bool>(10))

エラーを見ると、確かに右辺値参照を用いて左辺値参照を初期化しようとしているようなメッセージが出ている。やはりダメなようだ。

その他にもいろいろと手を動かして見たところ、以下の2つは問題なく動作する。

// ループ変数をconst auto&型にした場合
for(const auto& el : std::vector<bool>(10))
{
  /* Do something using "el". */
}

// ループ変数を参照ではなく値渡しにした場合
for(auto el : std::vector<bool>(10))
{
  /* Do something using "el". */
}

しかしよく分からないのは、コンテナに対してrange based forを回したときに右辺値参照が返ってくるというのは、一体どういう状況なのだろうか?そして、そいういうケースはどれくらい頻繁に起こりえるのだろうか?std::vectorはテンプレート特殊化によって少々直感的でない挙動をする特殊ケースだと思うのだが、そういったケースでしか右辺値参照を用いる機会がないのであれば、別にauto&を使っておけばよいような気がする。というのも、自分の開発チームが常にC++の達人揃いとは限らないからである。つまり、「そもそも右辺値参照って何?」というレベルの人がチームにいてもおかしくはない。そこで自分が一から右辺値参照とかperfect forwardingの説明をするには時間も実力も気力もないし、「とにかくおまじないと思ってauto&&を使って下さい」と言って納得してもらえるかも微妙である。であれば、そんなに問題がないのであれば、普段はatuo&を使っておけばよいという気がする。それだけでもrange based forの恩恵の大部分を受けられると思う。

Perfect forwardingはさておき、C++11からはコンテナの要素に対するイテレーションがずっと楽になった。こういう便利な新機能は今後もガンガン使っていきたい。欲をいうと、ループの中でループインデックスにアクセスできるともっとよいのだが・・・D言語にはその機能があるので、C++も今後同様の機能が追加されることを期待したい。