Here are ~100 lines of code implementing three classes (ZipRef
, ZipIter
, Zip
) which should satisfy the zip iterator pattern in a STL-compatible way. This means that, unlike boost::zip_iterator
, ZipIter
can be safely (I hope!) used in various algorithms like std::rotate
and std::sort
. I tried to embrace the power of C++17, aiming at writing much more readable (variadic) template code compared to what allowed by C++11.
Note: maintained on GitHub (includes a usage example and some extra notes), the code in this post is not going to be edited.
//
// C++17 implementation of ZipIterator by Dario Pellegrini <pellegrini.dario@gmail.com>
// Still unsure about the licence, but something in the line of just providing attribution
// October 2019
//
#include <tuple>
template <typename ...T>
class ZipRef {
std::tuple<T*...> ptr;
public:
ZipRef() = delete;
ZipRef(const ZipRef& z) = default;
ZipRef(ZipRef&& z) = default;
ZipRef(T* const... p): ptr(p...) {}
ZipRef& operator=(const ZipRef& z) { return copy_assign( z); }
ZipRef& operator=(const std::tuple<T...>& val) { return val_assign(val); }
template <size_t I = 0>
ZipRef& copy_assign(const ZipRef& z) {
*(std::get<I>(ptr)) = *(std::get<I>(z.ptr));
if constexpr( I+1 < sizeof...(T) ) return copy_assign<I+1>(z);
return *this;
}
template <size_t I = 0>
ZipRef& val_assign(const std::tuple<T...>& t) {
*(std::get<I>(ptr)) = std::get<I>(t);
if constexpr( I+1 < sizeof...(T) ) return val_assign<I+1>(t);
return *this;
}
std::tuple<T...> val() const {return std::apply([](auto&&...args){ return std::tuple((*args)...); }, ptr);}
operator std::tuple<T...>() const { return val(); }
template <size_t I = 0>
void swap(const ZipRef& o) const {
std::swap(*(std::get<I>(ptr)), *(std::get<I>(o.ptr)));
if constexpr( I+1 < sizeof...(T) ) swap<I+1>(o);
}
#define OPERATOR(OP) \
bool operator OP(const ZipRef & o) const { return val() OP o.val(); } \
inline friend bool operator OP(const ZipRef& r, const std::tuple<T...>& t) { return r.val() OP t; } \
inline friend bool operator OP(const std::tuple<T...>& t, const ZipRef& r) { return t OP r.val(); }
OPERATOR(==) OPERATOR(<=) OPERATOR(>=)
OPERATOR(!=) OPERATOR(<) OPERATOR(>)
#undef OPERATOR
};
template<typename ...IT>
class ZipIter {
std::tuple<IT...> it;
template<int N, typename... T> using NthTypeOf =
typename std::tuple_element<N, std::tuple<T...>>::type;
template<typename... T> using FirstTypeOf = NthTypeOf<0, T...>;
public:
using iterator_category = typename std::iterator_traits<FirstTypeOf<IT...>>::iterator_category;
using difference_type = typename std::iterator_traits<FirstTypeOf<IT...>>::difference_type;
using value_type = std::tuple<typename std::iterator_traits<IT>::value_type ...>;
using pointer = std::tuple<typename std::iterator_traits<IT>::pointer ...>;
using reference = ZipRef<typename std::iterator_traits<IT>::value_type ...>;
ZipIter() = default;
ZipIter(const ZipIter &rhs) = default;
ZipIter(ZipIter&& rhs) = default;
ZipIter(const IT&... rhs): it(rhs...) {}
ZipIter& operator=(const ZipIter& rhs) = default;
ZipIter& operator=(ZipIter&& rhs) = default;
ZipIter& operator+=(const difference_type d) {
std::apply([&d](auto&&...args){((std::advance(args,d)),...);}, it); return *this;
}
ZipIter& operator-=(const difference_type d) { return operator+=(-d); }
reference operator* () const {return std::apply([](auto&&...args){return reference(&(*(args))...);}, it);}
pointer operator->() const {return std::apply([](auto&&...args){return pointer (&(*(args))...);}, it);}
reference operator[](difference_type rhs) const {return *(operator+(rhs));}
ZipIter& operator++() { return operator+=( 1); }
ZipIter& operator--() { return operator+=(-1); }
ZipIter operator++(int) {ZipIter tmp(*this); operator++(); return tmp;}
ZipIter operator--(int) {ZipIter tmp(*this); operator--(); return tmp;}
difference_type operator-(const ZipIter& rhs) const {return std::get<0>(it)-std::get<0>(rhs.it);}
ZipIter operator+(const difference_type d) const {ZipIter tmp(*this); tmp += d; return tmp;}
ZipIter operator-(const difference_type d) const {ZipIter tmp(*this); tmp -= d; return tmp;}
inline friend ZipIter operator+(const difference_type d, const ZipIter& z) {return z+d;}
inline friend ZipIter operator-(const difference_type d, const ZipIter& z) {return z-d;}
#define OPERATOR(OP) \
bool operator OP(const ZipIter& rhs) const {return it OP rhs.it;}
OPERATOR(==) OPERATOR(<=) OPERATOR(>=)
OPERATOR(!=) OPERATOR(<) OPERATOR(>)
#undef OPERATOR
};
template<typename ...Container>
class Zip {
std::tuple<Container&...> zip;
public:
Zip() = delete;
Zip(const Zip& z) = default;
Zip(Zip&& z) = default;
Zip(Container&... z): zip(z...) {}
#define HELPER(OP) \
auto OP(){return std::apply([](auto&&... args){ return ZipIter((args.OP())...);}, zip);}
HELPER( begin) HELPER( end)
HELPER(rbegin) HELPER(rend)
#undef HELPER
};
#include <utility>
using std::swap;
template<typename ...T> void swap(const ZipRef<T...>& a, const ZipRef<T...>& b) { a.swap(b); }
#include <sstream>
template< class Ch, class Tr, class...IT, typename std::enable_if<(sizeof...(IT)>0), int>::type = 0>
auto& operator<<(std::basic_ostream<Ch, Tr>& os, const ZipRef<IT...>& t) {
std::basic_stringstream<Ch, Tr> ss;
ss << "[ ";
std::apply([&ss](auto&&... args) {((ss << args << ", "), ...);}, t.val());
ss.seekp(-2, ss.cur);
ss << " ]";
return os << ss.str();
}
Usage example from the README:
Consider this minimal example:
#include <vector> #include <string> #include <algorithm> #include <iostream> #include "ZipIterator.hpp" //Header only ;) int main() { std::vector<int> a{3,1,4,2}; std::vector<std::string> b{"Alice","Bob","Charles","David"}; auto zip = Zip(a,b); for (const auto & z: zip) std::cout << z << std::endl; std::cout << std::endl; std::sort(zip.begin(), zip.end()); for (const auto & z: zip) std::cout << z << std::endl; return 0; }
It can be compiled by simply enabling the c++17 (or more recent) standard and the produced output is:
$ g++ -std=c++17 main.cpp -o main.out && ./main.out [ 3, Alice ] [ 1, Bob ] [ 4, Charles ] [ 2, David ] [ 1, Bob ] [ 2, David ] [ 3, Alice ] [ 4, Charles ]
Behind the curtain the permutations (either by swapping or copying) applied by
std::sort
to the elements of vectora
have been simultaneously applied to vectorb
as well.Note that it would have been possible to zip a third (and more) vector as well, as the implementation leverages on variadic templates.
The reason for ZipRef
over std::tuple
is that one needs special constructor, assignment and comparison operators to be able to handle the tuple of pointers with a value-like semantics. One also needs a tuple of pointers because it allows to manipulate the data even if the tuple is constant, so one can extend the lifetime of non-const lvalues references of ZipRef (as the one returned when dereferencing ZipIter) by binding them to const references, while still being able, later, to modify the data being pointed to. Note that the custom swap, quite unusually, takes const references!