LoginSignup
24
29

More than 5 years have passed since last update.

range-based forをより使いやすくする

Last updated at Posted at 2016-03-07

range-based for(範囲ベースfor)、楽に綺麗に書けるので良く使ってます。
でも、使えない・使い難いことがあるので悔しいです。

  1. インデックス番号が欲しい時
  2. ループを分割したい時
  3. インクリメントのタイミングで何かしたい処理(continue時も含めて)

この3つを解決できましたので公開します。
まずは使用例から。

#include <iostream>
#include <list>
#include <string>
#include "rbfor_range.h"

int main()
{
    std::list<std::string> wList{"Lelouch", "Suzaku", "C.C.", "Karen", "Nunnally"};

    //   ↓★ループ分割の引継ぎ用変数           ↓★インクリメント時に実行したい処理
    auto wRBForIndexer = getRBForIndexer(wList, [](...){std::cout << "\n";});

    // ★最初のループ
    for (auto&& wLoop : wRBForIndexer)
    {        // ↑ループ変数
        auto& wString = wLoop.front();
        if (wString == "C.C.") {
            wLoop.pop_front();  // ←"C.C."の次から再開するためインクリメント
    break;
        }
        //                                ↓★インデックス番号
        std::cout << "1st Loop(" << wLoop.getIndex() << ") " << wString;
    }

    // ★継続ループ
    for (auto&& wLoop : wRBForIndexer)
    {
        auto& wString = wLoop.front();
        std::cout << "2nd Loop(" << wLoop.getIndex() << ") " << wString;
    }
    return 0;
}
コンパイルと実行:
msvc2015
> cl abstract.cpp /EHsc
> sample.exe
MinGW 5.2.0
> g++ abstract.cpp -std=c++11
> a.exe

結果:
1st Loop(0) Lelouch
1st Loop(1) Suzaku

2nd Loop(3) Karen
2nd Loop(4) Nunnally

rbfor_range.hとサンプルソースをここ(Gist)に上げています。

0.経緯

インデックス番号を取り出したい時、C言語時代に逆戻りしてインデックス番号で回してoperator[]で取り出してました。
長いこと"ベターC"erだったからというのもありますが、イテレータ構文が嫌いなんですよ。特にitr != end()itrがend()を越えたらどうしようって。無駄に悩みます。

そこで、teratailで聞いてみたところ、3つアイデアを頂けました。
①生配列、std::vector、std::arrayなら、ポインタにしてbegin()を引けばOK。
stack overflowに良い解があるよ
boostのadaptorもあるよ

listを使う予定もあり、結局②と③を参考にして作ってみました。(②③では分割やラムダ式追加ができない。)

範囲ベースfor専用 孫の手 RBForRangeです。("Range-Based For" Rangeの略です)

1.アイデア

基本的なアイデアは「レンジ」に基づいてます。レンジは将来的にイテレータを置き換えるとウワサされてます。
teratailでyohhoyさんから教えて頂いたのですが、Iterators Must Goが詳しいです。「Iterators Must Go」を訳してみたの人が訳してくれてます。レンジ、素晴らしいです。

さて、イテレータ・ベースのfor文の場合、当たり前ですがイテレータがループ変数になってますね。
標準の範囲ベースfor文にはループ変数がなく直接要素がでてきます。便利なのですが、それが仇になって使えない時があります。
そこで、「範囲ベースforのループ変数」として「レンジ」を出力するRBForRangeクラスを作りました。
①この「レンジ」にインデックス番号機能を仕込んでます。
②ループ変数を工夫してスマートに引き継ぎできるようにしました。
③開発中のソフトで欲しかったので、ついでにラムダ式も仕込んでみました。

「参照」を使うことで、中々手がとどかない背中(コンパイラが自動生成するコード)から更に手を伸ばして痒いところをを掻くような実装になってます。

2.まずはインデックス番号から

普通に範囲ベースforで下記のようなループを回してたとします。

#include <iostream>
#include <list>
#include <string>

int main()
{
    std::list<std::string> wList{"Lelouch", "Suzaku", "C.C.", "Karen", "Nunnally"};

//      ---<<< 通常の範囲ベースfor >>>---

    std::cout << "Normal range-based for\n";
    for (auto&& wString : wList)
    {
        if (wString == "C.C.")
    continue;
        std::cout << wString << "\n";
    }

   return 0;
}
//結果
//Normal range-based for
//Lelouch
//Suzaku
//Karen
//Nunnally

そして、ある日、仕様変更でインデックス番号が必要になりました。
範囲ベースforループの直前でインデックス番号を定義し、最後でインクリメントすることを考えたのですが、C.C.(しーつー)を隠すためのcontinueが邪魔をします。更に、悲しいかなstd::listにはoperator[]が無いので、indexでループしても取り出せません。
ですので、下記のように書きました。

//      ---<<< インデックス番号を追加するのでイテレータ・ベースのforへ変更 >>>---

    std::cout << "\nI want to Index-number.\n";
    std::size_t wIndex=0;
    for (auto itr=wList.begin(); itr != wList.end(); ++itr, ++wIndex)
    {
        auto& wString=*itr;

        if (wString == "C.C.")
    continue;
        std::cout << "(" << wIndex << ") " << wString << "\n";
    }
//結果
//I want to Index-number.
//(0) Lelouch
//(1) Suzaku
//(3) Karen
//(4) Nunnally

範囲ベースforに比べるとfor文が見づらくて悲しいです。wIndexがループ外にあるのも気分悪いです。どうにもむず痒いです。

そこで、孫の手の出番です。

//      ---<<< インデックス番号付きの範囲ベースfor >>>---

    std::cout << "\nIndexed range-based for\n";
    for (auto&& wLoop : getRBForIndexer(wList))
    {
        auto& wString = wLoop.front();

        if (wString == "C.C.")
    continue;
        std::cout << "(" << wLoop.getIndex() << ") " << wString << "\n";
    }
//結果
//Indexed range-based for
//(0) Lelouch
//(1) Suzaku
//(3) Karen
//(4) Nunnally

普通はコンテナ内の要素が出てくるのですが、範囲ベースfor専用の「レンジ」が出てくるようになります。

つまり、ループ変数wLoopには現在の「レンジ」が設定されていますので、現在の要素をwLoop.front()で取り出せ、インデックス番号をwLoop.getIndex()で取り出せます。

3.次に範囲ベースforループの分割

更に仕様変更が入り、インデックス番号要らないから、C.C.判定成功後は、無駄な判定を省略して高速化して欲しいそうです。ついでにフォーマットも変更とか。
ループ1つでやる場合はC.C.後を示すフラグが必要になりますので構造的にはあまり良くないです。素直にループを前半と後半に分けたいところです。

ま~かせて! 下記のように書けます。

//      ---<<< 範囲ベースforを2つのループに分割 >>>---

    std::cout << "\nSeparated range-based for\n";
    std::cout << "first loop\n";
    auto&& wRBForSeparator = getRBForSeparator(wList);
    for (auto&& wLoop : wRBForSeparator)
    {
        auto& wString = wLoop.front();

        if (wString == "C.C.") {
            wLoop.pop_front();
    break;
        }
        std::cout << "+++ " << wString << "\n";
    }
    std::cout << "second loop\n";
    for (auto&& wLoop : wRBForSeparator)
    {
        auto& wString = wLoop.front();

        std::cout << "*** " << wString << "\n";
    }
//結果
//Separated range-based for
//first loop
//+++ Lelouch
//+++ Suzaku
//second loop
//*** Karen
//*** Nunnally

孫の手ででてくるレンジですが、実は元のレンジと同じものです。つまり、wLoopwRBForSeparatorの参照なのです。

範囲ベースforの仕組みを利用して、ループを一度回る度に
wRBForSeparator.pop_front()され
wRBForSeparatorの参照がwLoopへ取り出される
ようにしています。

ですので、C.C.が見つかった時wLoopだけでなくwRBForSeparatorC.C.を指しています。なので、次のループでC.C.を表示させないためにwLoop.pop_front()して1つ進めています。
当然wLoop.pop_front()の代わりにwRBForSeparator.pop_front()としても同じ結果になります。同じものですから。

4.ついでに分割ループ+インデックス付き

そして、更に仕様変更が...やっぱりインデックスも表示したいそうです。
我侭ですね。よっしゃこいです!

//      ---<<< 範囲ベースforを2つのループに分割し、インデックス生成 >>>---

    std::cout << "\nSeparated Indexed range-based for\n";
    std::cout << "first loop\n";
    auto&& wRBForIndexer = getRBForIndexer(wList);
    for (auto&& wLoop : wRBForIndexer)
    {
        auto& wString = wLoop.front();

        if (wString == "C.C.") {
            wLoop.pop_front();
    break;
        }
        std::cout << "+" << wLoop.getIndex() << "+ " << wString << "\n";
    }
    std::cout << "second loop\n";
    for (auto&& wLoop : wRBForIndexer)
    {
        auto& wString = wLoop.front();

        std::cout << "*" << wLoop.getIndex() << "* " << wString << "\n";
    }
//結果
//Separated Indexed range-based for
//first loop
//+0+ Lelouch
//+1+ Suzaku
//second loop
//*3* Karen
//*4* Nunnally

5.++itrのタイミングで指定のラムダ式を実行する

ああ、執筆 仕様変更に疲れました。好きにさせて下さい。私はC.C.を見たいです。

//      ---<<< 範囲ベースforのインクリメント処理タイミングでラムダ式実行 >>>---

    std::cout << "\nNext-processed range-based for\n";
    for (auto&& wLoop : getRBForIndexer(wList,
        [](std::string const& iString){std::cout<<((iString == "C.C.")?", I love you!\n":"\n");}))
    {
        auto& wString = wLoop.front();
        std::cout << "[" << wLoop.getIndex() << "] " << wString;
    }
//結果
//Next-processed range-based for
//[0] Lelouch
//[1] Suzaku
//[2] C.C., I love you!
//[3] Karen
//[4] Nunnally

見ての通り、ラムダ式はループの最後で実行され、その回のループで使われた要素がバラメータで渡されます。

6.また別の機能を追加する

インデックス機能は、Indexerという下記のようなクラスをRBForRangeの基底クラスにすることで実現しています。

template<class tIterator, class tEnable=void>
class Indexer
{
    std::size_t mIndex;
public:
    Indexer(tIterator iBegin, tIterator iEnd) : mIndex(0)
    { }
    void pop_front()
    {
        ++mIndex;
    }
    std::size_t getIndex() const
    {
        return mIndex;
    }
};

これによりRBForRangeのコンストラクト時にmIndexを初期化し、必要に応じて更新しています。そして、getIndex()で返却してます。先述の2, 4, 5でgetIndex()を呼び出してインデックス番号を獲得してますが、それがこれです。

そして、ループ分割はIndexerの代わりにDoNothingという何もしないクラスをRBForRangeの基底クラスとして指定しています。

template<class tIterator, class tEnable=void>
struct DoNothing
{
    DoNothing(tIterator iBegin, tIterator iEnd) { }
    void pop_front() { }
};

つまり、必要であればIndexerとはまた異なる付加機能を追加することもできるのです。
使い方が少し大掛かりになるので、ほんとど使う機会はないと思いますが。

別機能を使いたい時は、追加機能を担うクラス・テンプレートを定義します。
これにはイテレータの型がテンプレート引数として渡されます。部分特殊化しやすいようイネーブル用のテンプレート・パラメータを設けてます。
例えば、今回のサンプルはstd::stringを要素とするコンテナに対応してますので、std::string用に部分特殊化して機能を定義してみました。

//      ---<<< プライマリー >>>---

template<class tIterator, class tEnable=void>
class Worker
{
public:
    Worker(tIterator iBegin, tIterator iEnd) { }
    void pop_front() { }
};

//      ---<<< std::stringを指すイテレータについて部分特殊化 >>>---

template<class tIterator>
class Worker
<
    tIterator,
    typename std::enable_if<std::is_same<std::string, ValueType<tIterator> >::value>::type
>
{
    tIterator   mBegin;
    ptrdiff_t   mRemaining;
public:
    Worker(tIterator const& iBegin, tIterator const& iEnd) : 
        mBegin(iBegin),
        mRemaining(std::distance(iBegin, iEnd)-1)
    { }
    void pop_front()
    {
        if (*mBegin == "C.C.")
            std::cout << " : No thank you.\n";
        else
            std::cout << "\n";
        ++mBegin;
        --mRemaining;
    }
    ptrdiff_t getRemaining()
    {
        return mRemaining;
    }
};

(ValueType<>はイテレータの型から要素の型を取り出すちょっとしたツールです。constとか外したいので独自に作りました。後述のソースにこっそり忍ばせてます。)

そして、下記のようにしてWorkerを使います。

//      ---<<< SeparatorやIndexerとはまた別の異なる処理 >>>---

    std::cout << "\nAnother functional range-based for\n";
    for (auto&& wLoop : getRBForRange<Worker>(wList))
    {
        auto& wString = wLoop.front();

        std::cout << "[" << wLoop.getRemaining() << "] " << wString;
    }
//結果
//Another functional range-based for
//[4] Lelouch
//[3] Suzaku
//[2] C.C. : No thank you.
//[1] Karen
//[0] Nunnally

このWorkerクラス、実は嫌いです。
"No thank you."をどこで表示しているのか分かり難く、ソースを読む人が苦労します。
ですので、これはあくまでも例です。こんな使い方したらダメです。("No thank you"とか...)

7.そしてソース

お待たせしました。下記がそのRBForRangeとその周辺のソースです。RBForRangeはコメント含めて200行ちょっとのrbfor_range.hです。ほとんどテンプレートなのでヘッダ・ファイルのみです。
あと説明に使ったサンプルのソースを1つにまとめてます。(sample.cpp

ここ(Gist)にあげてます。

Microsoft Visual Studio 2015 update1と、MinGW 5.2.0で下記コマンドでビルドして動作確認してます。

msvc
cl sample.cpp /EHsc
sample.exe
MinGW
g++ sample.cpp -std=c++11
a.exe

boostの技術をかなり参考にさせて貰ったので、rbfor_range.hBoost Software License, Version 1.0.にて公開します。基本的には改造や商用を含め自由に使って頂いて良いです。ソースを配布するときのみrbfor_range.hにつけている著作権表示をお願いします。(バイナリ配布の時は表示不要)
あるがままの提供で無保証です。

8.最後に

RBForRangeを範囲ベースfor専用と呼んでいるのには理由があります。
これはレンジの一種ですので、begin(), end()を持っています。範囲ベースforはこれを使っています。どのように使うのか標準規格で決まっていて、n2930: Range-based for loopについてが参考になります。

さて、RBForRangeのbegin(), end()は上記ソースにあるRangeReferencerクラスのインスタンスを返却します。
RangeReferencerはRBForRangeへの参照だけを持ってます。operator++()を呼ばれたらRBForRangeのpop_front()を呼び、operator!=()を呼ばれたらRBForRangeのempty()を呼んで反転して返却しています。
そして、operator*()はRBForRangeの参照をそのまま返却しています。これが、範囲ベースfor文のwLoopに設定されるのです。

普通のレンジのbegin(), end()はイテレータを返却しますが、このように全く働きが異なるRangeReferencerを返却します。しかも、begin(), end()どちらも同じインスタンスを返すのです。ということはend()のoperator++()が呼ばれてもpop_front()します。
なので、範囲ベースfor専用と位置づけ、begin(), end()を直接使用するのは禁止です。
できるものなら、begin(), end()をprivateに入れて範囲ベースforをフレンド登録したいです。

9.最後の最後に

特にitr != end()itrがend()を越えたらどうしようって。無駄に悩みます。

この件、対処してます。empty()の時にpop_front()したら、out_of_range例外を投げてます。これで一安心。
(実はこれ、イテレータではできないです。レンジだからこそできることの1つです。)

24
29
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
29