[C++]std::ranges::views::zip、enumerateの代替機能を作ってみる。

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

私はまだC++20を導入していないのだが、時代に取りに越されないために最低限の使い方を理解すべく、<ranges>で遊んでいる。ただ色々調べてみたところ、std::ranges::views::zipenumerateC++20に採択されなかったようだ。<ranges>の周りは何やらレビューが間に合わなかったらしく、viewsからコンテナに変換するstd::ranges::toも入っていない。
私が本格的にC++20を導入するのはおそらく3年くらい先のことになるが、一応ちまちました小さなプログラム中では利用するつもりなので、このあたりの機能は自力で取り揃えておくことにした。私は以前にちょうどrange based for loopで使えるzip、enumerate相当の機能を自作していたので、これを改良して作った。range-v3とかから拾ってこいって?ライセンスとか色々面倒くさそうだし、外部のライブラリへの依存を増やしたくないので却下だ。

実装

以前自作した機能は、まずstd::ranges::rangeの要件を満たしていなかった。rangeであるためにはstd::ranges::beginを呼び出すことができなければならず、そのために、C++20のイテレータ要件に対応した専用イテレータへと作り変える必要があった。
さらに、rangeであったとしてもstd::ranges::viewではない。rangeであるためには単にbegin、endの呼び出しが可能であれば良い。が、viewであるためにはこれに多くの条件が加わる。movableであること、default_initializableであること、そして(ユーザーが特殊化していなければ)view_baseの派生クラスであることだ。
通常、単なるrangeからviewへと変換するにはstd::views::allなどを通せばよいのだろうが、std::views::allは一時オブジェクトを渡すにはいくつかの条件をクリアしなければならない。そのため自作機能をrangeのまま使うことは困難で、std::ranges::viewを満足するように作り変える必要があった。
したがって、std::ranges::view_interfaceを継承し、デフォルト初期化可能に変更しなければならない。view_interfaceもview_baseの派生クラスなので、これを継承すればview_base派生という条件は自動的に満たされる。このあたりは一つ前の記事を参照されたい。
しかし私は普段、C++20を利用できない場合のほうが多い。そこでこれを<ranges>に対応させたいときはマクロUSE_CPP20_RANGESを定義させ、定義されていない場合はC++17の範囲内でrange based for loopに対応するように書いた。

なお、<ranges>対応部分以外は私がまだstd::rangesもPythonのzipやenumerateも知らなかった頃に自己流で作ったものなので、それらとはやや動作が異なるし、<ranges>から独立している部分の名前はzipでもenumerateでもない。

#include <iostream>
#include <vector>
#include <map>
#include <tuple>
#include <type_traits>
#include <optional>

#define USE_CPP20_RANGES

#ifdef USE_CPP20_RANGES
#include <ranges>
#define NS_STD_RANGES std::ranges::
#else
#define NS_STD_RANGES std::
#endif

namespace detail
{

template <class T>
struct DecayRRef { using Type = T; };
template <class T>
struct DecayRRef<T&&> { using Type = std::decay_t<T>; };

template <class ...Args>
auto MakeDecayedTuple(Args&& ...args)
{
    return std::tuple<typename DecayRRef<Args>::Type...>(std::forward<Args>(args)...);
}

template <class Iterators, class IndexSequence>
class BundledIterator_impl;
template <class Iterators, class IndexSequence>
class BundledSentinel_impl;

template <class ...Iterators, std::size_t ...Indices>
class BundledIterator_impl<std::tuple<Iterators...>, std::index_sequence<Indices...>>
{
public:

    using iterator_category = std::common_type_t<typename Iterators::iterator_category...>;
    using difference_type = typename std::tuple_element_t<0, std::tuple<Iterators...>>::difference_type;
    using value_type = std::tuple<typename Iterators::value_type...>;
    using reference = std::tuple<std::iter_reference_t<Iterators>...>;

    BundledIterator_impl() = default;
    BundledIterator_impl(Iterators... cs)
        : mIterators(cs...)
    {}

    BundledIterator_impl& operator++()
    {
        [[maybe_unused]] int d[] = { (++std::get<Indices>(mIterators), 0)... };
        return *this;
    }
    BundledIterator_impl operator++(int)
    {
        BundledIterator_impl res = *this;
        [[maybe_unused]] int d[] = { (++std::get<Indices>(mIterators), 0)... };
        return res;
    }
    auto operator*() const noexcept
    {
        //std::ranges::viewは場合によってはoperator*によって一時オブジェクトを返してくる場合がある。
        //その場合、ここをstd::forward_as_tupleにするとrvalue referenceのダングリングが発生する。
        //よって、各iteratorのoperator*戻り値がrvalueである場合は参照を取り払ってtupleにするため、MakeDecayedTupleを使う。
        return MakeDecayedTuple(*std::get<Indices>(mIterators)...);
    }
    bool operator==(const BundledIterator_impl& it) const
    {
        return (... && (std::get<Indices>(mIterators) == std::get<Indices>(it.mIterators)));
    }
    bool operator!=(const BundledIterator_impl& it) const
    {
        return !(*this == it);
    }

    template <size_t Index>
    decltype(auto) GetIteratorAt() const { return std::get<Index>(mIterators); }

private:
    std::tuple<Iterators...> mIterators;
};

template <class ...Iterators, std::size_t ...Indices>
class BundledSentinel_impl<std::tuple<Iterators...>, std::index_sequence<Indices...>>
{
    using BundledIterator_ = BundledIterator_impl<std::tuple<Iterators...>, std::index_sequence<Indices...>>;
public:

    BundledSentinel_impl() = default;
    BundledSentinel_impl(Iterators... cs)
        : mIterators(cs...)
    {}
    friend bool operator==(const BundledIterator_& it, const BundledSentinel_impl& end)
    {
        //一つでも最終要素を超えれば終端とみなす。
        return (... || (it.GetIteratorAt<Indices>() == std::get<Indices>(end.mIterators)));
    }

private:
    std::tuple<Iterators...> mIterators;
};

}

template <class ...Iterators>
using BundledIterator = detail::BundledIterator_impl<std::tuple<Iterators...>, std::make_index_sequence<sizeof...(Iterators)>>;
template <class ...Iterators>
using BundledSentinel = detail::BundledSentinel_impl<std::tuple<Iterators...>, std::make_index_sequence<sizeof...(Iterators)>>;


template <class ...Iterators>
class BundledIteratorWithIndex
    : public BundledIterator<Iterators...>
{
public:

    using value_type = std::tuple<size_t, typename Iterators::value_type...>;

    BundledIteratorWithIndex() = default;
    BundledIteratorWithIndex(Iterators... cs) : BundledIterator<Iterators...>(cs...), mIndex(0) {}

    BundledIteratorWithIndex& operator++()
    {
        ++mIndex;
        BundledIterator<Iterators...>::operator++();
        return *this;
    }
    BundledIteratorWithIndex operator++(int)
    {
        BundledIteratorWithIndex res = *this;
        ++mIndex;
        BundledIterator<Iterators...>::operator++();
        return res;
    }
    auto operator*() const noexcept
    {
        return std::tuple_cat(std::make_tuple(mIndex), BundledIterator<Iterators...>::operator*());
    }

private:
    std::size_t mIndex;
};

//sentinelの方にindexの情報は必要ないので、BundledSentinelをそのまま使う。
template <class ...Iterators>
using BundledSentinelWithIndex = BundledSentinel<Iterators...>;

namespace detail
{

template <class ...Iterators>
BundledIterator<Iterators...> MakeBundledIterator(Iterators ...it)
{
    return BundledIterator<Iterators...>(it...);
}
template <class ...Iterators>
BundledSentinel<Iterators...> MakeBundledSentinel(Iterators ...it)
{
    return BundledSentinel<Iterators...>(it...);
}
template <class ...Iterators>
BundledIteratorWithIndex<Iterators...> MakeBundledIteratorWithIndex(Iterators ...it)
{
    return BundledIteratorWithIndex<Iterators...>(it...);
}
template <class ...Iterators>
BundledSentinelWithIndex<Iterators...> MakeBundledSentinelWithIndex(Iterators ...it)
{
    return BundledSentinelWithIndex<Iterators...>(it...);
}


template <class Containers, class IndexSequence>
class BundledRange_impl;
template <class ...Containers, std::size_t ...Indices>
class BundledRange_impl<std::tuple<Containers...>, std::index_sequence<Indices...>>
{
    //std::ranges::viewの要件を満たすためには、このクラスがデフォルト初期化可能でなければならない。
    //しかしBundledRangeは場合によって一時オブジェクトを持たなければならないことがあり、
    //デフォルト初期化不可能な一時オブジェクトを与えられるとコンパイルができなくなるし、
    //参照を与えられた場合もポインタに変換しなければならない。
    //諸々の違いを吸収するために、Holder、Observer、std::optionalを使い分ける。
    template <class C>
    struct Holder
    {
        C c;
        C& operator*() { return c; }
        const C& operator*() const { return c; }
    };
    template <class C>
    struct Observer
    {
        std::add_pointer_t<C> c;
        Observer() : c(nullptr) {}
        Observer(C c) : c(&c) {}
        C& operator*() { return *c; }
        const C& operator*() const { return *c; }
    };

    template <class C, bool IsLValueReference, bool IsDefaultInitializable>
    struct PNO;
    template <class C, bool IsDefaultInitializable>
    struct PNO<C, true, IsDefaultInitializable> { using Type = Observer<C>; };
    template <class C>
    struct PNO<C, false, true> { using Type = Holder<C>; };
    template <class C>
    struct PNO<C, false, false> { using Type = std::optional<C>; };

#ifdef USE_CPP20_RANGES
    template <class C>
    using PNO_t = typename PNO<C, std::is_lvalue_reference_v<C>, std::default_initializable<C>>::Type;
#else
    //rangesを使わないのならデフォルト初期化可能である必要はないので、optionalは不要であり、常にIsDefaultInitializable=trueでよい。
    template <class C>
    using PNO_t = typename PNO<C, std::is_lvalue_reference_v<C>, true>::Type;
#endif
public:

    BundledRange_impl() = default;
    template <class ...C, std::enable_if_t<(sizeof...(C) > 0), std::nullptr_t> = nullptr>
    BundledRange_impl(C&&... c)
        : mContainers(std::forward<C>(c)...) {}

    auto begin() const { return MakeBundledIterator(NS_STD_RANGES begin(*std::get<Indices>(mContainers))...); }
    auto end() const { return MakeBundledSentinel(NS_STD_RANGES end(*std::get<Indices>(mContainers))...); }

protected:

    std::tuple<PNO_t<Containers>...> mContainers;
};

}

template <class ...Containers>
using BundledRange = detail::BundledRange_impl<std::tuple<Containers...>, std::make_index_sequence<sizeof...(Containers)>>;

#ifdef USE_CPP20_RANGES
namespace myviews
{

template <std::ranges::input_range ...Ranges>
class zip : public std::ranges::view_interface<zip<Ranges...>>
{
    using BundledRange_ = BundledRange<Ranges...>;
public:

    zip() = default;
    template <class ...C, std::enable_if_t<(sizeof...(C) > 0), std::nullptr_t> = nullptr>
    zip(C&& ...c)
        : mRange(std::forward<C>(c)...)
    {}
    zip(BundledRange_&& r)
        : mRange(std::move(r))
    {}

    auto begin() const { return NS_STD_RANGES begin(mRange); }
    auto begin() { return NS_STD_RANGES begin(mRange); }
    auto end() const { return NS_STD_RANGES end(mRange); }
    auto end() { return NS_STD_RANGES end(mRange); }

private:

    BundledRange_ mRange;
};

template <class ...C> zip(C&& ...)->zip<C...>;

}
#endif

namespace detail
{

template <class Iterators, class IndexSequence>
class BundledRangeWithIndex_impl;
template <class ...Containers, std::size_t ...Indices>
class BundledRangeWithIndex_impl<std::tuple<Containers...>, std::index_sequence<Indices...>>
    : public BundledRange<Containers...>
{
public:

    using BundledRange<Containers...>::BundledRange;
    using Base = BundledRange<Containers...>;

    auto begin() const
    {
        return MakeBundledIteratorWithIndex(NS_STD_RANGES begin(*std::get<Indices>(this->mContainers))...);
    }
    auto end() const
    {
        return MakeBundledSentinelWithIndex(NS_STD_RANGES end(*std::get<Indices>(this->mContainers))...);
    }

};

}

template <class ...Containers>
using BundledRangeWithIndex = detail::BundledRangeWithIndex_impl<std::tuple<Containers...>, std::make_index_sequence<sizeof...(Containers)>>;

#ifdef USE_CPP20_RANGES
namespace myviews
{

template <std::ranges::input_range ...Ranges>
class enumerate : public std::ranges::view_interface<enumerate<Ranges...>>
{
    using BundledRangeWithIndex_ = BundledRangeWithIndex<Ranges...>;
public:

    enumerate() = default;
    template <class ...C, std::enable_if_t<(sizeof...(C) > 0), std::nullptr_t> = nullptr>
    enumerate(C&& ...c)
        : mRange(std::forward<C>(c)...)
    {}
    enumerate(BundledRangeWithIndex_&& r)
        : mRange(std::move(r))
    {}

    auto begin() const { return NS_STD_RANGES begin(mRange); }
    auto begin() { return NS_STD_RANGES begin(mRange); }
    auto end() const { return NS_STD_RANGES end(mRange); }
    auto end() { return NS_STD_RANGES end(mRange); }

private:

    BundledRangeWithIndex_ mRange;
};

template <class ...C> enumerate(C&& ...)->enumerate<C...>;

}
#endif

template <class ...Containers>
BundledRange<Containers...> BundleRange(Containers&& ...cs)
{
    return BundledRange<Containers...>(std::forward<Containers>(cs)...);
}

template <class ...Containers>
BundledRangeWithIndex<Containers&&...> BundleRangeWithIndex(Containers&& ...cs)
{
    return BundledRangeWithIndex<Containers...>(std::forward<Containers>(cs)...);
}

#undef NS_STD_RANGES


int main()
{
    std::vector<int> v1{ 4, 2, 3, 1, 5, 8, 7 };
    std::vector<int> v2{ 1, 2, 3, 4, 5 };
    for (auto [vv1, vv2] : myviews::zip(v1, v2) |
                           std::views::filter([](auto tup) { auto [vv1, vv2] = tup; return vv1 != vv2; }))
    {
        //値が異なるもののみを取り出して、v1の方を修正する。
        //ただしv1とv2はsizeが異なるので、範囲は小さい方(v2)に合わせられる。
        printf("[%d, %d] ", vv1, vv2);
        vv1 = vv2;
    }
    printf("\n");
    //[4, 1] [1, 4]

    for (auto [vv1, vv2] : myviews::zip(v1, v2))
    {
        //値がきちんと揃ったことを確認。
        printf("[%d, %d] ", vv1, vv2);
    }
    printf("\n");
    //[1, 1] [2, 2] [3, 3] [4, 4] [5, 5] 

    for (auto [i, vv1, vv2] : myviews::enumerate(v1, std::views::all(v2) |
                              std::views::transform([](auto i) { return i * 2; })))
    {
        printf("[[%zu] %d, %d] ", i, vv1, vv2);
    }
    printf("\n");
    //[[0] 1, 2] [[1] 2, 4] [[2] 3, 6] [[3] 4, 8] [[4] 5, 10]

    return 0;
}

解説

まず上では、BundledRange、BundledRangeWithIndexという2つのクラスを定義している。それぞれがzip、enumerateに近い動作をするrangeである。これらはstd::ranges::rangeの要件を満たしているが、std::ranges::viewを満たしてはいない。C++17でも動作させるためにstd::ranges::view_interfaceを継承していないからである。
そこで、これらをラップしたmyview::zipmyview::enumerateを作った。これらはview_interfaceを継承しつつ、各々がBundleRange、BundleRangeWithIndexをメンバ変数として持っているだけだ。多重継承でもいいような気はするがやめておいた。あるいはUSE_CPP20_RANGESが定義されているときだけBundledRangeにview_interfaceを継承させることもできたが、マクロで継承関係が切り替わるのもなんとなく気持ちが悪いのでやめた。
またイテレータはそれぞれBundledIterator、BundledIteratorWithIndexである。複数のイテレータをtupleに束ねただけの安易な設計だ。
ただし、end関数が返すのはBundledIteratorではなくBundledSentinelとなっている。これはoperator==の動作を調整するためである。通常、std::tuple<Iterators...>の形に束ねられているイテレータの一致を調べるためには、すべてのイテレータが一致するかどうかを調べるべきである。しかしzipの動作の場合、個々のイテレータのうち一つでも範囲をはみ出てしまったらループを打ち切らなければならない。よって、このような末尾判定を行うためのSentinelを用意した。

というわけで、他のstd::viewsとも連携した動作が可能になっていることを確認したところで本記事は終わりである。ただし上の実装ではstd::forward_iteratorの要件しか満たしていないので、より多彩な動作に対応させたければもう少し調整が必要だろう。