Tech Racho エンジニアの「?」を「!」に。
  • 開発

[C++] イテレーターを作る【BPS Advent Calendar: 12/20】

はじめまして、yoshiと申します。昨日のAdvent Calenderのyoshi.kさんとは別人です。

BPSでは Ruby on Rails による高速開発を行っているので、TechRachoの技術系の記事もRubyの話題が多く含まれ、すると読者層もRuby開発者が多いのかな、と思っているのですが、私はRubyは全く分かりません。

現在私は弊社製品の超縦書の開発に携わっており、そこではC++を利用しています。大体C++のことしか書けない人間ですので、この記事でもC++の話をさせてもらおうかと思います。

今回の記事は、そんなに深い話はしません。C++ガチ勢の方には今更な、物足りない内容だと感じるかもしれませんが、ご了承ください。

なお、この記事では、特に明示しない場合、C++14を基準にしたいと思います。

そもそもイテレーターとは

イテレーター、日本語では反復子とも言いますが、C++でプログラミングをしているとよく出会うことかと思います。

イテレーターについてざっくり説明すると、「ある集合の要素を順番に列挙する仕組みのオブジェクト」のことです。
C++に限らず、様々な言語で同じような機能は実装されていますが、C++においては、イテレーターは「ポインタと同じような動きをすることができるクラス」という設計思想になっています。
ただし、すべてのイテレーターがポインタと同じ動きを実装されているとは限りません。

イテレーターは、その機能によって、以下の5つのカテゴリーに分けられています。

  • 入力イテレーター(Input iterator)
  • 出力イテレーター(Output iterator)
  • 前方イテレーター(Forward iterator)
  • 双方向イテレーター(Bidirectional iterator)
  • ランダムアクセスイテレーター(Random-access iterator)

イテレーターの例

イテレーターは、標準ライブラリのコンテナオブジェクトを操作する時によく使われます。

たとえば、std::vector<int>の中身を一個ずつ列挙してカンマ区切りで標準出力に出力したい時は、以下のような関数を作れば実行できます。

void output_vector_values(const std::vector<int> &vec) {
    // イテレーターの型名。あえて示しましたが、autoで推論することもできます
    using iterator = std::vector<int>::const_iterator;
    for (iterator itr = vec.begin(), end = vec.end();
         itr != end;
         ++itr) {

        std::cout << *itr << ", ";
    }
}

ちなみに、ちょっと冗長に書きましたが、C++11からは範囲for文(range-based-for)という糖衣構文が用意されたので、上記のコードは下記のように書き換えることができます。

void output_vector_values(const std::vector<int> &vec) {
    for (int value : vec) {
        std::cout << vec << ", ";
    }
}

イテレーターが消えてしまいましたね。ですが、内部的にはbegenendが呼ばれてイテレーターを使っているので、見た目上コードが簡潔にはなりましたがイテレーターが不要になった訳ではありません。

最低限のイテレーター

C++の規格では、上述したように5つのイテレーターのカテゴリーが定義されていて、それぞれが満たすべき要件が挙げられています。
ですが、range-based-for文で利用可能なイテレーターの要件を考えると、それよりもずっと少ない機能を満たすだけでよいということがわかります。ここでは、これを「最低限のイテレーター」と呼ぶことにします。

最低限のイテレーターが満たすべき要件は以下のようになります。

  • コピー可能である
  • 前置++演算子を利用してインクリメントを行うことができる
  • 単項*演算子で値を参照できる
  • !=演算子で比較することができる

例として、int型の整数を小さい順に列挙する最低限のイテレーターを考えてみます。

class my_iterator {
private:
    int i = 0;

public:
    my_iterator() = default;
    explicit my_iterator(int val): i(val) {}
    my_iterator &operator++() {
        ++i;
        return *this;
    }
    int operator *() const {
        return i;
    }
    bool operator !=(const my_iterator &rhs) const {
        return i != rhs.i;
    }
};

標準ライブラリのイテレーターと互換性のあるイテレーターを用意しようと思うと、iterator_categoryvalue_typeなどの型情報がstd::iterator_traitsを通して取得できるようにしなければなりませんが、range-based-forの中で使うだけの用途であれば、これで十分でしょう。

range-based-for文では、:の右に渡された値に対して、自動的にbeginend関数が呼ばれます。つまり、beginendが呼ばれた時にこのmy_iterator型を返すような型とbegin/end関数を定義すればよいのです。

class myclass {
private:
    int i;
public:
    explicit myclass(int val): i(val) {}
    my_iterator begin() { return my_iterator(0); }
    my_iterator end() { return my_iterator(i); }
};

int main() {
    for(int i : myclass(42)) {
        std::cout << i << " ";
    }
    std::cout << std::endl;
}

実際にオンラインコンパイラで動作させてみました

応用 ~FizzBuzzをやってみる~

イテレーターが作れるようになれば、任意のイテレーターをラップして新しいイテレーターを作れるようになります。

my_iteratorをラップしてfizzbuzz_iteratorを作ってみましょう。

class fizzbuzz_iterator {
private:
    my_iterator itr;
public:
    fizzbuzz_iterator(my_iterator iterator) : itr(iterator) {}
    fizzbuzz_iterator &operator ++() {
        ++itr;
        return *this;
    }
    std::string operator* () const {
        int i = *itr;
        if (!(i % (3*5))) {
            return "FizzBuzz";
        } else if (!(i % 3)) {
            return "Fizz";
        } else if (!(i % 5)) {
            return "Buzz";
        } else {
            return std::to_string(i);
        }
    }
    bool operator !=(const fizzbuzz_iterator &rhs) {
        return itr != rhs.itr;
    }
};

class fizzbuzz {
private:
    int i;
public:
    explicit fizzbuzz(int val): i(val) {}
    fizzbuzz_iterator begin() {
        return fizzbuzz_iterator(my_iterator(1));
    }
    fizzbuzz_iterator end() {
        return fizzbuzz_iterator(my_iterator(i + 1));
    }
};

int main() {
    for(std::string str : fizzbuzz(42)) {
        std::cout << str << " ";
    }
    std::cout << std::endl;
}

こちらもオンラインコンパイラで動作させてみました

このように、イテレーターをラップして、値を操作したり、フィルターをかけるようなこともできます。テンプレートクラスにすればもっと柔軟にできるでしょう。

標準ライブラリはイテレーターを前提にした実装も多いので、自前で定義できるようになっておくと色々とできることが増えるのではないでしょうか。

ただし、標準ライブラリで使うためのイテレーターは、最低限のイテレーターではなく、もう少し複雑な定義をしなければいけない場合もあります。

今日のところはここまでとしたいと思います。またそのうち新しい記事を書くかもしれません。

関連記事


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。