[C++]<ranges>のstd::viewsと同様に扱えるviewを自作する。

※本記事の内容はC++20の<ranges>を全く理解できていない人間が手探りで書いたものです。参考にするのは構いませんが鵜呑みにしないでください。

本記事はこの次の記事(C++20に入らなかったzip、enumerateの代替機能)の下準備である。あれをstd::viewsに対応させるにはviewそのものを自作しなければならなかったので、その方法を簡単にまとめる。

view_interfaceの基本

viewの自作のためにはstd::ranges::view_interfaceを使う。これは演算子などを勝手に用意してくれるBoost Operators Libraryのように、CRTPを使ってviewに必要な機能を取り揃えてくれるものだ。これを使って、とりあえず非常に簡素な、iteratorでコンテナを全走査するだけのviewを作ってみる。

#include <ranges>
#include <vector>
#include <iostream>

template <class Container>
class MyView : public std::ranges::view_interface<MyView<Container>>
{
public:

    MyView() = default;//デフォルトコンストラクタは必須。
    MyView(const Container& c) : mBegin(c.begin()), mEnd(c.end()) {}

    auto begin() const { return mBegin; }
    auto end() const { return mEnd; }

private:
    typename Container::const_iterator mBegin, mEnd;
};

int main()
{
    std::vector<int> v{ 1, 2, 3, 4, 5 };
    for (auto& vv : MyView(v))
        std::cout << vv << " ";
    std::cout << std::endl;
    //1 2 3 4 5 
    for (auto& vv : MyView(v) | std::views::filter([](int i) { return i % 2 == 1; }))
        std::cout << vv << " ";
    std::cout << std::endl;
    //1 3 5 
    return 0;
}

"MyView"という名前のviewを自作している。ちゃんとstd::views::filterとも連携できており、viewとして機能していることが分かる。
必要なのはデフォルトコンストラク1メンバ関数begin、endの3つだ。あるクラスがviewであるためには以下の条件を満たす必要があるのだが、そのために定義しなければならないのがこの3つなのである。

  1. std::default_initializableである。
  2. std::movableである。
  3. std::ranges::rangeの条件を満たす。
  4. view_baseクラスを継承している(ユーザーがstd::ranges::enable_viewを特殊化していない場合)。

1つ目の条件はデフォルトコンストラクタがあれば良い。2つ目はまあいいとして、3つ目の条件を満たすためにはstd::ranges::beginstd::ranges::endの2つの関数呼び出しが可能でなければならない。単純な条件ではあるが、begin関数が返すイテレータC++20のstd::input_or_output_iterator2という要件を満たしている必要がある。C++17以前のイテレータは、C++20では要件の変更によりイテレータと見做されなくなっている可能性があるため注意を要する。
4つ目はユーザーが変更することも可能であるらしいが、素直にview_interfaceを継承するほうが良いだろう。view_interfaceはview_baseの派生クラスなので、これを継承すれば4つ目の条件は満たされる。

このときbegin()によって返されるイテレータの種類によって、以下のメンバ関数が自動的に定義される。

  1. empty(実質無条件)
  2. operator bool(実質無条件)
  3. data(contiguous_iteratorである場合)
  4. size(sized_rangeである場合)
  5. front(実質無条件)
  6. back(bidirectional_rangeかつcommon_rangeである場合)
  7. operator[](random_access_rangeである場合)

それぞれの条件の意味は下記の参考ページを読んだほうが分かりやすいので、ここでは省略する。

ちょっと凝ったviewを作る

view_interfaceの基本が分かったところで、ではもう少しだけ凝った動作をするviewを考えてみたい。上の例は単に何らかのコンテナをviewに見せかけるだけの役に立たないものだった。しかし標準ライブラリ中に実際に用意されているviewはもっと多彩な機能を持ち、受け取ったrangeを加工しながら走査することができる。例えばfilter_viewなどはどのように動作しているのだろう?あれを再現することはできないだろうか?そのためには、begin、end関数が返すイテレータの方を改めて自作しなければならない。
ここからはろくに資料が見つからない中での本当に手探りの作業だ。間違いだらけかも知らんので安易に信用してはいけない。

必要最小限の部分のみで作ってみると、以下のようになった。

#include <ranges>
#include <vector>
#include <iostream>

template <std::ranges::input_range Range, class Func>
class MyFilterView : public std::ranges::view_interface<MyFilterView<Range, Func>>
{
public:
    class MyFilterIterator
    {
    public:

        //簡単のためforward_iterator扱いにする。
        using iterator_category = std::forward_iterator_tag;
        using value_type = std::ranges::range_value_t<Range>;
        using difference_type = std::ranges::range_difference_t<Range>;

        MyFilterIterator() : mParent(nullptr) {}
        MyFilterIterator(MyFilterView& parent, std::ranges::iterator_t<Range> current)
            : mParent(&parent), mCurrent(current)
        {}
        MyFilterIterator& operator++()
        {
            mCurrent = std::find_if(std::move(++mCurrent), std::ranges::end(mParent->mRange), mParent->mFunc);
            return *this;
        }
        MyFilterIterator operator++(int)
        {
            MyFilterIterator res = *this;
            ++(*this);
            return res;
        }
        bool operator==(const MyFilterIterator& it) const
        {
            return mCurrent == it.mCurrent;
        }
        std::ranges::range_reference_t<Range> operator*() const
        {
            return *mCurrent;
        }

    private:
        MyFilterView* mParent;
        std::ranges::iterator_t<Range> mCurrent;
    };
    class MyFilterSentinel
    {
    public:

        MyFilterSentinel() = default;
        MyFilterSentinel(MyFilterView& parent)
            : mLast(std::ranges::end(parent.mRange))
        {}

        friend bool operator==(const MyFilterIterator& it, const MyFilterSentinel& se)
        {
            return it == se.mLast;
        }

    private:
        std::ranges::sentinel_t<Range> mLast;
    };

    MyFilterView() = default;
    MyFilterView(Range r, Func f)
        : mRange(r), mFunc(f)
    {}

    auto begin()
    {
        auto front = std::find_if(std::ranges::begin(mRange), std::ranges::end(mRange), mFunc);
        return MyFilterIterator(*this, front);
    }
    auto end()
    {
        if constexpr (std::ranges::common_range<Range>)
            return MyFilterIterator{ *this, std::ranges::end(mRange) };
        else
            return MyFilterSentinel{ *this };
    }

private:
    Range mRange;
    Func mFunc;
};

template <class Func>
class MyFilter
{
public:

    MyFilter(Func f)
        : mFunc(f)
    {}

    //パイプライン風に繋げていくための演算子オーバーロード。
    template <std::ranges::input_range Range, class Func_>
    friend auto operator|(Range&& r, MyFilter<Func_> f);

private:
    Func mFunc;
};

template <std::ranges::input_range Range, class Func_>
auto operator|(Range&& r, MyFilter<Func_> f)
{
    return MyFilterView<Range, Func_>(std::forward<Range>(r), std::move(f.mFunc));
}

int main()
{
    std::vector<int> v{ 1, 2, 3, 4, 5 };
    for (auto vv : std::views::all(v) |
                   MyFilter([](int i) { return i % 2 == 1; }) |
                   std::views::transform([](int i) { return i * 10; }))
        std::cout << vv << " ";
    std::cout << std::endl;
    //10 30 50
    return 0;
}

新たにMyFilterViewを作成した。std::ranges::filter_viewもどきである。
MyFilterViewそれ自体は先のMyViewと大きくは変わらない。ただしMyFilterIteratorという独自のイテレータを持っており、begin、end両関数が返す値はこれらのインスタンスとなっている3。この動作の肝はbeginとoperator++だ。通常は先頭から一つずつ移動すべきところを、std::find_ifで検索するようにしただけ。viewは単に、パイプラインを通じて受け取ったrangeの走査範囲を、任意のイテレータによってちょこちょこと弄っているだけのようである。

参考

https://onihusube.hatenablog.com/entry/2020/12/27/150400C++20のイテレータ
https://en.cppreference.com/w/cpp/ranges(rangesの基本)
https://en.cppreference.com/w/cpp/ranges/view_interface(view_interfaceについて)
https://zenn.dev/onihusube/articles/6608a0185832dc51213c(view_interfaceについて)


  1. デフォルト初期化が不可能な場合、viewのコンセプトを満足しない。もし自前でコンストラクタを定義する場合はデフォルトコンストラクタも用意しておくこと。

  2. C++20においてイテレータに課せられる最小限の要件。つまり、すべてのC++20イテレータはこれを満たしていなければならない。詳しくは参考ページを。

  3. ただしstd::rangesのイテレータはbeginとendの戻り値の型が一致するとは限らないので、不一致のときのためにMyFilterSentinelを定義しており、endはこれを返す場合もある。