I worked out a zip
operator similar to Python's, because I didn't find one in std
. It allows to use range-based for
loops to iterate at once over several equal-length containers (arrays, counters... anything that has an iterator and a static length). It should be safe (never exceed the iterator's capacity), be able to modify the content of the container in-place when possible, and have no run-time overhead compared to manually incrementing the iterators.
Some things still look a bit fishy to me and I also wonder if all my naming/implementation choices adhere the std
look-and-feel. Examples of use:
std::array a = {1,2,3,4};
std::array b = {4,3,2,1};
for (auto [i, j, k] : zip(a, b, a)) {
std::cout << i << " " << j << " " << k << std::endl;
i = 42; // we can overwrite the values of a
}
//// This one doesn't work yet:
// for (auto [i, j] : zip(a, {4, 3, 2, 1})) {
// std::cout << i << " " << j << std::endl;
// }
With it comes a simple range
class that allows to include counters in the iterations:
// x takes the value of array a, and i counts from 0 to 3
for (auto [x, i] : zip(a, range<4>())) {
std::cout << i << " " << x << std::endl;
}
Note here that the arguments to zip
aren't necessarily l-values.
Here is my implementation:
// inductive case
template<typename T, typename... Ts>
struct zip : public zip<Ts...> {
static_assert(std::tuple_size<T>::value == std::tuple_size<zip<Ts...>>::value,
"Cannot zip over structures of different sizes");
using head_value_type = std::tuple<typename T::value_type&>;
using tail_value_type = typename zip<Ts...>::value_type;
using value_type = decltype(std::tuple_cat(std::declval<head_value_type>(),
std::declval<tail_value_type>()));
zip(T& t, Ts&... ts) : zip<Ts...>(ts...), t_(t) {}
zip(T& t, Ts&&... ts) : zip<Ts...>(ts...), t_(t) {}
zip(T&& t, Ts&... ts) : zip<Ts...>(ts...), t_(t) {}
zip(T&& t, Ts&&... ts) : zip<Ts...>(ts...), t_(t) {}
struct iterator {
using head_iterator = typename T::iterator;
using tail_iterator = typename zip<Ts...>::iterator;
head_iterator head;
tail_iterator tail;
bool operator!=(iterator& that) { return head != that.head; }
void operator++() { ++head; ++tail; }
value_type operator*() {
return std::tuple_cat<head_value_type, tail_value_type>(*head, *tail);
}
iterator(head_iterator h, tail_iterator t) : head(h), tail(t) {}
};
iterator begin() { return iterator(t_.begin(), zip<Ts...>::begin()); }
iterator end() { return iterator(t_.end(), zip<Ts...>::end()); }
T& t_;
};
// base case
template<typename T>
struct zip<T> {
using value_type = std::tuple<typename T::value_type&>;
using iterator = typename T::iterator;
zip(T&& t) : t_(t) {};
zip(T& t) : t_(t) {};
iterator begin() { return t_.begin(); }
iterator end() { return t_.end(); }
private:
T& t_;
};
// must implement tuple_size to check size equality
template<typename T, typename... Ts>
struct std::tuple_size<zip<T, Ts...>> {
static constexpr int value = std::tuple_size<T>::value;
};
What looks fishy/over-complicated:
- the constructors to cover all kinds of arguments (l/r-value/references)
- the mangling of tuple types
- bonus: why doesn't my second example compile?
For completeness, here is my implementation of the range
class:
template<class T, T BEG, T END, T STEP>
struct Range {
Range() {};
using iterator = Range;
using value_type = T;
bool operator!=(iterator that) { return this->val_ < that.val_; }
void operator++() { val_ += STEP; }
int& operator*() { return val_;}
iterator begin() { return *this; }
iterator end() { return Range(END); }
private:
Range(int val) : val_(val) {}
T val_ = BEG;
};
template<class T, T BEG, T END, T STEP>
struct std::tuple_size<Range<T, BEG, END, STEP>> {
static constexpr int value = (END - BEG) / STEP;
};
template<class T, T BEG, T END, T STEP>
static auto range() { return Range<T, BEG, END, STEP>(); };
template<int BEG, int END, int STEP=1>
static auto range() { return Range<int, BEG, END, STEP>(); };
template<int END>
static auto range() { return Range<int, 0, END, 1>(); };
Any feedback will be much appreciated! Thanks in advance.