※本記事の内容をC++20の<ranges>
に対応するように更新した記事があります。C++20を使用中の方はそちらを参照してください。->std::ranges::views::zip、enumerateの代替機能を作ってみる。
私はrange-based for loopを十分に扱えていなかったのだと思い知った今日このごろ。
range-based for loopは単純なループを書きたいときはとても便利である。ただし単純に使うと色々な問題が発生し、結局はiteratorだったり、従来のカウンター式ループを使ってしまうことも多いだろう。
私の場合、頻繁に遭遇していたのは、サイズの等しい複数のコンテナのループだ。同じサイズのstd::vectorやstd::list、std::mapなど諸々のコンテナがあるときに、それらを同時に走査することが出来ないのだ。いや、出来ない、と思っていた。
他にも走査しているindexの情報がほしいという要求は頻繁に出てくるらしい。range-based for loopは単純に使うと要素への参照しか受け取れないので、場所を知ることは出来ない。もし走査している対象がstd::vectorだったらポインタを使って一瞬で計算できるが、これはstd::vectorの要素がメモリ上で必ず連続配置されているからこその芸当である。
しかしこんなもの、range-based for loopの動作を理解すれば単純な話だった。複数のイテレータやindex情報を束ねた合成イテレータを作ってしまえばいい。
#include <vector> #include <map> #include <list> #include <iostream> namespace detail { template <class Iterators, class IndexSequence> class BundledIterator_impl; template <class ...Iterators, std::size_t ...Indices> class BundledIterator_impl<std::tuple<Iterators...>, std::index_sequence<Indices...>> { public: BundledIterator_impl(Iterators... cs) : mIterators(cs...) {} BundledIterator_impl& operator++() { int d[] = { (++std::get<Indices>(mIterators), 0)... }; return *this; } auto operator*() const noexcept { return std::forward_as_tuple(*std::get<Indices>(mIterators)...); } bool operator==(const BundledIterator_impl& it) const { return std::get<0>(mIterators) == std::get<0>(it.mIterators); } bool operator!=(const BundledIterator_impl& it) const { return !(*this == it); } 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> class BundledIteratorWithIndex : public BundledIterator<Iterators...> { public: BundledIteratorWithIndex(Iterators... cs) : BundledIterator<Iterators...>(cs...), mIndex(0) {} BundledIteratorWithIndex& operator++() { ++mIndex; BundledIterator<Iterators...>::operator++(); return *this; } auto operator*() const noexcept { return std::tuple_cat(std::make_tuple(mIndex), BundledIterator<Iterators...>::operator*()); } private: std::size_t mIndex; }; namespace detail { template <class ...Iterators> BundledIterator<Iterators...> MakeBundledIterator(Iterators ...it) { return BundledIterator<Iterators...>(it...); } template <class ...Iterators> BundledIteratorWithIndex<Iterators...> MakeBundledIteratorWithIndex(Iterators ...it) { return BundledIteratorWithIndex<Iterators...>(it...); } template <class Iterators, class IndexSequence> class BundledRange_impl; template <class ...Containers, std::size_t ...Indices> class BundledRange_impl<std::tuple<Containers...>, std::index_sequence<Indices...>> { public: template <class ...C> BundledRange_impl(C&&... c) : mContainers(std::forward<C>(c)...) {} auto begin() const { return MakeBundledIterator(std::get<Indices>(mContainers).begin()...); } auto end() const { return MakeBundledIterator(std::get<Indices>(mContainers).end()...); } private: std::tuple<Containers&&...> mContainers; }; } template <class ...Containers> using BundledRange = detail::BundledRange_impl<std::tuple<Containers...>, std::make_index_sequence<sizeof...(Containers)>>; 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: template <class ...C> BundledRangeWithIndex_impl(C&&... c) : mContainers(std::forward<C>(c)...) {} auto begin() const { return MakeBundledIteratorWithIndex(std::get<Indices>(mContainers).begin()...); } auto end() const { return MakeBundledIteratorWithIndex(std::get<Indices>(mContainers).end()...); } private: std::tuple<Containers&&...> mContainers; }; } template <class ...Containers> using BundledRangeWithIndex = detail::BundledRangeWithIndex_impl<std::tuple<Containers...>, std::make_index_sequence<sizeof...(Containers)>>; 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)...); } int main() { std::vector<double> v{ 1,2,3,4,5,6,7,8 }; std::map<int, double> m{ {8,8},{7,7},{6,6},{5,5},{4,4},{3,3},{2,2},{1,1} }; std::list<float> l{ 11,12,13,14,15,16,17,18 }; for (auto[vv, mm, ll] : BundleRange(v, m, l)) { std::cout << ++vv << ", {" << mm.first << "," << ++mm.second << "}, " << ++ll << std::endl; } for (auto[i, vv, mm, ll] : BundleRangeWithIndex(v, m, l)) { std::cout << "[" << i << "]: " << ++vv << ", {" << mm.first << "," << ++mm.second << "}, " << ++ll << std::endl; } for (auto[vv, mm, ll] : BundleRange(v, m, l)) { std::cout << ++vv << ", {" << mm.first << "," << ++mm.second << "}, " << ++ll << std::endl; } return 0; }
BundleRange関数、またはBundleRangeWithIndex関数に適当なコンテナを与えるだけでよい。それぞれPythonでいうところのzip、enumerateに相当する機能だ。
ただしこのとき、上の実装では第1引数に与えたコンテナのサイズが基準となるため、2つ目以降のコンテナのサイズが1つ目のそれ未満である場合範囲外アクセスとなるため注意されたい。for文中でのイテレータの要素を受け取る変数の宣言は、C++17以降であれば構造化束縛を用いて非常に簡単に書くことができる。そうでない場合、この合成イテレータは各要素をstd::tupleに左辺値参照もしくは右辺値参照として纏めて返してくるので、std::tieでもstd::getでも好きなものを使って値を取り出せば良い。
range-basedとか仰々しい名前がつけられたところで、本質的にはiteratorと同じである。begin()、end()、インクリメントなどを勝手に実行してくれているだけでしかないのだ。従って、要求に沿ったイテレータらしきものを作れば、それに従って動作してくれる。
多分こんなシンプルな機能は既にBoostあたりに実装されているのだろうが、私を含めてBoostに依存できないという縛りがある人々にとっては、こういう小技は案外重要である。