[C++]多重ループを一つに纏める直積集合イテレータを作ってみた。

 Range-based for loopを理解してからというもの、その悪用を色々と思いついてしまって。
 今日も小ネタである。実用性はあんまりない。多重ループにすれば良いところを、敢えてそれを統合し一つのループで表現したくなってしまった病的C++erのお遊びだ。唯一メリットがあるとすれば、多重ループでは難しいcontinueによる全ループの脱出に対応するところか。

 次のような状況を考えてみたい。

std::vector<double> x;
std::vector<double> y;
std::vector<double> z;
//x, y, zには何らかの値が格納されているものとする。
for (auto x_ : x)
{
    for (auto y_ : y)
    {
        for (auto z_ : z)
        {
            //何らかの処理。
        }
    }
}

 ごく一般的な多重ループだ。訓練されたC++プログラマーであれば別段苦にはしないだろう。
 ただ、コードの可読性よりも見栄えの美しさに興味が湧いてきてしまった人間は、その不格好さが気になり始めるのだ。次のように書けないのだろうか。

for (auto [x_, y_, z_] : (x, y, zの積集合))
{
    //何らかの処理。
}

 というわけでちょっと作ってみた。

#include <tuple>
#include <vector>
#include <iostream>

namespace detail
{

template <class ...Iterators>
class ProductIterator
{
public:

    ProductIterator(std::tuple<Iterators...> begin, std::tuple<Iterators...> end)
        : mIterators(begin), mBegin(begin), mEnd(end)
    {}
    ProductIterator(std::tuple<Iterators...> begin, std::tuple<Iterators...> end, std::tuple<Iterators...> current)
        : mIterators(current), mBegin(begin), mEnd(end)
    {}
private:
    template <size_t Index, std::enable_if_t<(Index != 0), std::nullptr_t> = nullptr>
    ProductIterator& Increment()
    {
        auto& it = std::get<Index>(mIterators);
        ++it;
        if (it == std::get<Index>(mEnd))
        {
            it = std::get<Index>(mBegin);
            return Increment<Index - 1>();
        }
        return *this;
    }
    template <size_t Index, std::enable_if_t<(Index == 0), std::nullptr_t> = nullptr>
    ProductIterator& Increment()
    {
        ++std::get<0>(mIterators);
        return *this;
    }
public:
    ProductIterator& operator++()
    {
        return Increment<sizeof...(Iterators) - 1>();
    }
private:
    template <size_t ...Indices>
    auto GetContents(std::index_sequence<Indices...>) const noexcept
    {
        return std::forward_as_tuple(*std::get<Indices>(mIterators)...);
    }
public:
    auto operator*() const noexcept
    {
        return GetContents(std::make_index_sequence<sizeof...(Iterators)>());
    }
    bool operator==(const ProductIterator& it) const
    {
        return mIterators == it.mIterators;
    }
    bool operator!=(const ProductIterator& it) const
    {
        return !(*this == it);
    }

private:
    const std::tuple<Iterators...> mBegin;
    std::tuple<Iterators...> mIterators;
    const std::tuple<Iterators...> mEnd;
};

template <class ...Iterators>
auto MakeProductIterator(std::tuple<Iterators...> begin, std::tuple<Iterators...> end)
{
    return ProductIterator<Iterators...>(begin, end);
}
template <class ...Iterators>
auto MakeProductIterator(std::tuple<Iterators...> begin, std::tuple<Iterators...> end, std::tuple<Iterators...> current)
{
    return ProductIterator<Iterators...>(begin, end, current);
}

template <class Containers, class Indices>
class ProductRange;
template <class ...Containers, size_t ...Indices>
class ProductRange<std::tuple<Containers...>, std::index_sequence<0, Indices...>>
{
public:
    template <class ...C>
    ProductRange(C&& ...c)
        : mContainers(std::forward<C>(c)...)
    {}

    auto begin() const
    {
        auto beg = std::make_tuple(std::get<0>(mContainers).begin(), std::get<Indices>(mContainers).begin()...);
        auto end = std::make_tuple(std::get<0>(mContainers).end(), std::get<Indices>(mContainers).end()...);
        return MakeProductIterator(beg, end);
    }
    auto end() const
    {
        auto beg = std::make_tuple(std::get<0>(mContainers).begin(), std::get<Indices>(mContainers).begin()...);
        auto end = std::make_tuple(std::get<0>(mContainers).end(), std::get<Indices>(mContainers).end()...);
        auto cur = std::make_tuple(std::get<0>(mContainers).end(), std::get<Indices>(mContainers).begin()...);
        return MakeProductIterator(beg, end, cur);
    }
private:
    std::tuple<Containers...> mContainers;
};

}

template <class ...Containers>
auto MakeProductRange(Containers&& ...c)
{
    return detail::ProductRange<std::tuple<Containers...>, std::make_index_sequence<sizeof...(Containers)>>(std::forward<Containers>(c)...);
}

int main()
{
    std::vector<int> x{ 1, 2, 3 };
    std::vector<int> y{ 4, 5, 6 };
    std::vector<int> z{ 7, 8, 9 };

    for (auto [x_, y_, z_] : MakeProductRange(x, y, z))
    {
        printf("%d * %d * %d = %3d\n", x_, y_, z_, x_ * y_ * z_);
    }

    return 0;
}

 x、y、zの三重ループを巡回するのと同じような振る舞いをする特殊なイテレータを作っている。一応見た目はすっきりした。
 しかしあくまで疑似三重ループでしかないので、多重ループ中のbreakやcontinueのように一つのループのみを脱出するという動作に対応しない。不格好さを許せるのなら実装する手段はありそうだが、私にはどうせ使う機会がないので作らなかった。もしより多彩な機能を欲するのなら上のコードを適当に改造してほしい。

 私はC++だけでなくPythonも使うことがあるのだが、Visual StudioPythonのコードを編集しているとインデントによるブロックを調整するのがとても面倒くさく感じてくる。C++であれば{}を書き込むことで自動的に中身をフォーマットしてくれる。しかしPythonはそのような明確な区切り文字がないので、いちいち自分でインデントを調整しなければならない。もし既に書かれたコードの外側にループを追加したい場合、範囲内のブロック全てに手作業でインデントを追加しなければならなかったりする(選択範囲に一括でインデントを追加する機能はある)。とにかく面倒なのだ。スクリプト言語の“簡潔さ”というメリットがIDEとの相性の悪さによって死んでいる。
 せめて多重ループを何とかしたいと思い探してみると、Pythonにはitertools.productという関数があるらしいことを知った。直積のリストを生成するだけの雑な機能ではあるが、インデントを省略できる点はありがたかった。実行速度は悪化しそうな気もするがPythonを使う時点でお察しなので気にしていない。
 これをC++で再現してみようと考えたわけである。しかし高速化に狂っているC++er的には直積のstd::vectorを作るなんて外道な方法を採用するわけにはいかないので、上述のようなイテレータの形にしてみた。雑な方法だがまあ一応動作するし、お遊びとしては及第点ではなかろうか。