├── .gitignore ├── CMakeLists.txt ├── LICENSE.md ├── README.md ├── test_zip_tuple.cpp ├── test_zip_two.cpp ├── zip_tuple.hpp └── zip_two.hpp /.gitignore: -------------------------------------------------------------------------------- 1 | build -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) 2 | cmake_minimum_required(VERSION 3.12) 3 | project(ZipIterator) 4 | 5 | set(FETCHCONTENT_QUIET OFF) 6 | include(FetchContent) 7 | FetchContent_Declare(googletest SYSTEM 8 | GIT_REPOSITORY https://github.com/google/googletest 9 | GIT_TAG v1.13.x 10 | GIT_PROGRESS TRUE 11 | ) 12 | FetchContent_MakeAvailable(googletest) 13 | 14 | add_executable(test_zip_two test_zip_two.cpp) 15 | target_compile_features(test_zip_two PRIVATE cxx_std_17) 16 | target_link_libraries(test_zip_two PRIVATE 17 | gtest 18 | gtest_main 19 | pthread) 20 | 21 | add_test(test_zip_two test_zip_two) 22 | 23 | 24 | add_executable(test_zip_tuple test_zip_tuple.cpp) 25 | target_compile_features(test_zip_tuple PRIVATE cxx_std_17) 26 | target_link_libraries(test_zip_tuple PRIVATE 27 | gtest 28 | gtest_main 29 | pthread) 30 | 31 | add_test(test_zip_tuple test_zip_tuple) 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020 CommitThis Ltd 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | C++: Zip Iteration 2 | ================== 3 | This document is licensed CC-BY-NC-SA 3.0, everything else is licensed under 4 | the MIT terms. 5 | 6 | Python is one of my favourite languages. One of the features it has is the `zip` 7 | function, which allows for parallel iteration of arrays and I wondered if 8 | something like that was possible in C++. While I haven't seen anything like this 9 | before and I didn't really go to the trouble of researching this; I thought it 10 | would be an interesting exercise I could try and do myself. 11 | 12 | In any case, using some modern C++ techniques we can do this reasonably easily. 13 | Being C++, I am not terribly confident I've covered all of the various details 14 | properly, and any feedback on this would be appreciated. 15 | 16 | > While the value categories in C++ aren't too complicated, I have simplified 17 | > the categories to an __lvalue__ (that which is named, and has a predictable 18 | > address, e.g. `int x = 1`), and an __rvalue__ (that which does not, is a 19 | > temporary; is a new instance returned by a function e.g. `f(){ return 1; }`. 20 | 21 | The Loop 22 | -------- 23 | This was the motivating example, use structured bindings to unpack values from 24 | two iterables in parallel. 25 | 26 | for (auto && [x, y] : zip(a, b)) { 27 | c.push_back(x + z); 28 | } 29 | 30 | I wanted both `x` and `y` to be references and therefore modifiable, but only if 31 | the respective zipped containers was passed in as non-const, and values 32 | otherwise. 33 | 34 | A range-based for loop executes functions over a range, using syntactic sugar to 35 | do so. It requires that the range expression represents a suitable sequence, 36 | e.g. an array or object with `begin` and `end` functions available. 37 | 38 | https://en.cppreference.com/w/cpp/language/range-for 39 | 40 | Therefore the `zip` function ought to return such an object. It will be a 41 | temporary (it is an __rvalue__), but that is ok as it's lifetime is extended until 42 | the end of the loop. The `begin` and `end` functions of this object must then 43 | return suitable iterators. 44 | 45 | The `zip` call also needs to be able to handle arguments of different types, 46 | and not require the type be supplied at use. 47 | 48 | Finally, the `value_type` of the iterated sequence must be de-structurable, 49 | which can be an array, tuple-like, or an object with non-static data members. 50 | 51 | https://en.cppreference.com/w/cpp/language/structured_binding 52 | 53 | So, three components will be needed: 54 | * A utility function for users to call that handles type deduction; 55 | * An object that meets the requirements for range-for that returns 56 | * A suitable iterator that proxies the iterators for the passed in types. 57 | 58 | 59 | The Utility Function 60 | -------------------- 61 | 62 | template 63 | auto zip(T && t, U && u) { 64 | return zipper{std::forward(t), std::forward(u)}; 65 | } 66 | 67 | Universal references are used to accept arguments of any type. This is not 68 | because we want to accept temporaries (which does appear to work), but because 69 | we want to accept both const and non-const types. We could be more clever at 70 | this point to be more strict with some more template trickery, but I think this 71 | is good enough for this example. 72 | 73 | In any case, we can see that the types of the arguments are deduced and are then 74 | used as template arguments to the object that returns the iterators. 75 | 76 | 77 | The Zipper 78 | ---------- 79 | I couldn't think of a great name for this, but basically is a really simple 80 | object that contains references (or values, largely depends on what the type 81 | arguments are) to the objects to be iterated over, and returns our special 82 | zip iterators. 83 | 84 | template 85 | class zipper { 86 | public: 87 | using Iter1 = select_iterator_for; 88 | using Iter2 = select_iterator_for; 89 | 90 | using zip_type = zip_iterator; 91 | 92 | template 93 | zipper(V && a, W && b) 94 | : m_a{a} 95 | , m_b{b} 96 | {} 97 | 98 | auto begin() -> zip_type { 99 | return zip_type{std::begin(m_a), std::begin(m_b)}; 100 | } 101 | auto end() -> zip_type { 102 | return zip_type{std::end(m_a), std::end(m_b)}; 103 | } 104 | 105 | private: 106 | T m_a; 107 | U m_b; 108 | }; 109 | 110 | The only trickery here is making sure we select the create iterator type to be 111 | used by the proxy. If we pass in a `std::vector<>&`, we want to use the 112 | `std::vector<>::iterator` type, however if it's a `std::vector<> const &` then 113 | we need to select `std::vector<>::const_iterator`. A relatively simple use of 114 | `std::conditional_t` solves this: 115 | 116 | template 117 | using select_iterator_for = std::conditional_t< 118 | std::is_const_v>, 119 | typename std::decay_t::const_iterator, 120 | typename std::decay_t::iterator>; 121 | 122 | This selects the correct iterator type. Basically, if what's passed in is const, 123 | the `const_iterator` is selected and otherwise `iterator`. The reason why we 124 | `std::decay_t` is used is because the types are references, and you can't access 125 | their type definitions as it's considered an incomplete type. `std::decay_t` 126 | returns it's fundamental type and solves this issue. 127 | 128 | > As per https://en.cppreference.com/w/cpp/types/is_const, the reference needs 129 | > to be removed from the original type, although I'm not sure why! 130 | 131 | 132 | The Zip Iterator 133 | ---------------- 134 | The zip iterator holds two iterators for the actual type being iterated over, 135 | and proxies their usual functions in tandem. 136 | 137 | template 138 | class zip_iterator 139 | { 140 | public: 141 | using value_type = std::pair< 142 | select_access_type_for, 143 | select_access_type_for 144 | >; 145 | 146 | zip_iterator() = delete; 147 | 148 | zip_iterator(Iter1 iter_1_begin, Iter2 iter_2_begin) 149 | : m_iter_1_begin {iter_1_begin} 150 | , m_iter_2_begin {iter_2_begin} 151 | {} 152 | 153 | auto operator++() -> zip_iterator& { 154 | ++m_iter_1_begin; 155 | ++m_iter_2_begin; 156 | return *this; 157 | } 158 | 159 | auto operator++(int) -> zip_iterator { 160 | auto tmp = *this; 161 | ++*this; 162 | return tmp; 163 | } 164 | 165 | auto operator!=(zip_iterator const & other) { 166 | return !(*this == other); 167 | } 168 | 169 | auto operator==(zip_iterator const & other) { 170 | return 171 | m_iter_1_begin == other.m_iter_1_begin || 172 | m_iter_2_begin == other.m_iter_2_begin; 173 | } 174 | 175 | auto operator*() -> value_type { 176 | return value_type{*m_iter_1_begin, *m_iter_2_begin}; 177 | } 178 | 179 | private: 180 | Iter1 m_iter_1_begin; 181 | Iter2 m_iter_2_begin; 182 | }; 183 | 184 | Again, fairly straight forward. The iterator is constructed from two iterators, 185 | which are incremented whenever the iterator itself is incremented. It's not 186 | really a proper iterator in the sense that it doesn't meet the minimum 187 | requirements for such (e.g. doesn't have a category), but it is a minimal 188 | example that works. 189 | 190 | 191 | The comparison we need to make in order to detect when iteration should end is 192 | when any of the iterators equal their end position. This isn't strictly correct 193 | from a semantic point of view -- it returns true as soon as any of the contained 194 | iterators equal their respective pair. This is necessary as the distance between 195 | the starting points of either may be different, and then they would never be 196 | equal at the same time leading to dereferencing past the end, and the loop never 197 | terminating. 198 | 199 | De-referencing the iterator returns a `std::pair` containing either references 200 | or values corresponding to the internal de-referenced iterators. 201 | 202 | The consideration here is what the access type is when the internal iterators 203 | are is de-referenced. In most cases this should be a reference, but this is not 204 | always possible. For example, `std::vector` is a special case in that it 205 | doesn't actually store `bool`s as you might think. It does some special bit 206 | twiddling to save on space (although arguably this is implementation defined). 207 | A normal iterator of such is incapable of returning a reference, and 208 | de-referencing returns a value. 209 | 210 | template 211 | using select_access_type_for = std::conditional_t< 212 | std::is_same_v::iterator> || 213 | std::is_same_v::const_iterator>, 214 | typename Iter::value_type, 215 | typename Iter::reference 216 | >; 217 | 218 | The access type selection is there because I was originally zipping a vector of 219 | `int`s and a vector of `bool`s. `std::vector` has a specialisation for `bool` 220 | that means that the value can't be accessed by reference, but instead by value. 221 | This is obviously very specific for a `std::vector`, but this could either be 222 | extended or replaced and I think works quite well to demonstrate how the access 223 | type could be changed. This could, however, be surprising for users. 224 | 225 | 226 | What Is The Loop Actually Doing? 227 | -------------------------------- 228 | * First, the `zip` function is called that deduces the passed in typed which are 229 | then used to create: 230 | * The `zipper` object. This contains either a reference or value to what's been 231 | passed in and has `begin` and `end` functions which the range-for uses to 232 | generate a loop; 233 | * On each pass of the loop, the iterator from `begin` is dereferenced and 234 | assigned to the value on the left hand side (the "range declaration") and 235 | finally incremented; 236 | * The range declaration is a structured binding, and the temporary `std::pair` 237 | from the dereferenced iterator is unpacked into it. 238 | 239 | 240 | There is also some strangeness going on with the structured bindings. If one of 241 | the destructured variables is a reference, then a const qualification will have 242 | effect. If it is a value (e.g. in the case of `std::vector`), then the 243 | const qualifier is applied. 244 | 245 | Further, the binding will fail if it is qualified as a non-const __lvalue__ 246 | reference. 247 | 248 | The Story So Far... 249 | ------------------- 250 | In any case, at this point, we should be able to iterate in parallel over two 251 | containers: 252 | 253 | auto a = std::vector{1, 2, 3, 4, 5, 6}; 254 | auto const & b = a; 255 | auto c = std::vector{}; 256 | 257 | for (auto && [x, y] : zip(a, b)) { 258 | c.push_back(x + y); 259 | } 260 | 261 | This works as you might expect, `x` and `y` are either const or non-const 262 | references. What's curious here is how the structured binding is used. 263 | Ideally, if you wanted to iteration and wanted to enforce const on any of the 264 | values you might write something like: 265 | 266 | for (auto const & [x, y] : zip(a, b)) ... 267 | 268 | The `const` in this case refers to the type being unpacked, which is a temporary 269 | `std::pair`. This will have no affect whatsoever on the actual types it 270 | contains. If you want to enforce this, you would need to make sure that the 271 | passed in type (`a` or `b`) are actually const references. 272 | 273 | Additionally, because the returned type is a temporary, you will never be able 274 | to bind it to an __lvalue__ reference. 275 | 276 | But what about..... 277 | 278 | 279 | Iterating Over More Than Two Types 280 | ================================== 281 | This is slightly more complicated than the previous example. I won't go to the 282 | trouble of repeating what i've said earlier, however the full source can be 283 | found on ######## 284 | 285 | The utility function is modified to take a variable amount of arguments: 286 | 287 | template 288 | auto zip(T && ... t) { 289 | return zipper{std::forward(t)...}; 290 | } 291 | 292 | In the zipper class, instead of storing the container/references in named 293 | members, we can use a tuple instead. 294 | 295 | template 296 | class zipper { 297 | public: 298 | using zip_type = zip_iterator ...>; 299 | 300 | // ... snip ... 301 | 302 | private: 303 | std::tuple m_args; 304 | }; 305 | 306 | As for creating the iterator through `begin` and `end`, we have to find some way 307 | of unpacking the tuple in order to call these functions on the underlying types. 308 | 309 | C++17 has a utility function that can do this: `std::apply`. It takes a lambda 310 | expression as the first argument (which in itself takes as arguments the values 311 | of the tuple) and the tuple as the second: 312 | 313 | std::apply([](auto && ... args){ /* more magic here */ }, my_tuple); 314 | 315 | The arguments can then be expanded as an ordinary parameter pack's using 316 | `std::begin(args)...`: 317 | 318 | auto begin() -> zip_type { 319 | return std::apply([](auto && ... args){ 320 | return zip_type(std::begin(args)...); 321 | }, m_args); 322 | } 323 | 324 | So, basically what's going on is that the tuple is being turned into a 325 | parameter pack that's expanded in the constructor for our custom iterator. 326 | 327 | With respect to the zip iterator we use a tuple member to store the iterators, 328 | and use the same technique to provide `operator*` and `operator++`: 329 | 330 | template 331 | class zip_iterator 332 | { 333 | public: 334 | 335 | // ... snip ... 336 | 337 | auto operator++() -> zip_iterator& { 338 | std::apply([](auto && ... args){ ((args += 1), ...); }, m_iters); 339 | return *this; 340 | } 341 | 342 | auto operator*() -> value_type { 343 | return std::apply([](auto && ... args){ 344 | return value_type(*args...); 345 | }, m_iters); 346 | } 347 | }; 348 | 349 | The use of this technique with `operator*` is exactly the same, only we're 350 | de-referencing the internal iterators and unpacking the results into yet another 351 | tuple. With `operator++`, we're not returning anything, and just applying the 352 | same operator to the contained iterators. 353 | 354 | Finally, the equivalence operator is a special case. In a twist of fate, we also 355 | need to do parallel iteration on all of the iterators! We can't use `std::apply` 356 | as, as far as I can tell, only works on one tuple at a time. With a somewhat 357 | terrifying set of template functions: 358 | 359 | template 360 | auto any_match_impl( 361 | std::tuple const & lhs, 362 | std::tuple const & rhs, 363 | std::index_sequence) -> bool 364 | { 365 | auto result = false; 366 | result = (... | (std::get(lhs) == std::get(rhs))); 367 | return result; 368 | } 369 | 370 | template 371 | auto any_match( 372 | std::tuple const & lhs, 373 | std::tuple const & rhs) -> bool 374 | { 375 | return any_match_impl(lhs, rhs, std::index_sequence_for{}); 376 | } 377 | 378 | The second function is a helper to call the actual implementation, so that a 379 | sequence of integers is generated with respect to the size of the template 380 | arguments. 381 | 382 | > At this point, both the tuples are the same type and therefore have exactly 383 | > the same size. 384 | 385 | In the implementation function, a fold expression is used to expand the index 386 | sequence; comparing the type instances of each tuple at the same index, which is 387 | aggregated into the result. This was probably the most difficult thing I had to 388 | deal with throughout; it wasn't an expansion I was at all familiar with, and I 389 | apologise in advance if I haven't explained it properly! 390 | 391 | 392 | Conclusion 393 | ========== 394 | Hopefully, I have demonstrated how parallel iteration over multiple types is 395 | possible. We can now quite happily do something like this (observe the varying 396 | sizes!): 397 | 398 | auto a = std::vector{1, 2, 3, 4, 5, 6}; 399 | auto b = std::vector{1, 2, 3, 4, 5, 6, 7}; 400 | auto c = std::vector{0, 0, 0, 0, 0}; 401 | auto const & d = b; 402 | 403 | for (auto && [x, y, z] : c9::zip(a, d, c)) { 404 | z = x + y; 405 | } 406 | 407 | auto expected = std::vector{2, 4, 6, 8, 10}; 408 | assert(c == expected); 409 | 410 | There are two ways I can currently see that could be improved: 411 | * Making the iterators have knowledge of their end position. This could be used 412 | to make the equality operation semantically correct in cases (which I can't 413 | see as being useful) and also help with runtime safety; 414 | * One of the concepts of C++ is flexible operations over data; you may want to 415 | "zip" across ranges within containers, that aren't either at the begin or end 416 | positions. In which case you might want a `zip` signature that looks like 417 | `zip(begin_a, end_a, begin_b, end_b, ...)`. This would be easy for two sets of 418 | data, but might be more complicated for the variadic case. -------------------------------------------------------------------------------- /test_zip_tuple.cpp: -------------------------------------------------------------------------------- 1 | #include "zip_tuple.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | #include 10 | 11 | 12 | 13 | 14 | TEST(ZipTuple, CanIterateVector) 15 | { 16 | auto a = std::vector{1, 2, 3, 4, 5, 6}; 17 | auto b = std::vector{1, 2, 3, 4, 5, 6}; 18 | 19 | auto expected = std::vector{2, 4, 6, 8, 10, 12}; 20 | auto result = std::vector{}; 21 | 22 | for (auto && [i, j] : c9::zip(a, b)) { 23 | result.push_back(i + j); 24 | } 25 | EXPECT_EQ(result, expected); 26 | } 27 | 28 | 29 | TEST(ZipTuple, CanAssignZippedByReference) 30 | { 31 | auto a = std::vector{1, 2, 3, 4, 5, 6}; 32 | auto b = std::vector{1, 2, 3, 4, 5, 6}; 33 | auto c = std::vector{0, 0, 0, 0, 0, 0}; 34 | 35 | for (auto && [i, j, k] : c9::zip(a, b, c)) { 36 | k = i + j; 37 | } 38 | 39 | auto expected = std::vector{2, 4, 6, 8, 10, 12}; 40 | EXPECT_EQ(c, expected); 41 | } 42 | 43 | 44 | TEST(ZipTuple, WillIterateOverShortestSize) 45 | { 46 | auto a = std::vector{1, 2, 3, 4, 5, 6}; 47 | auto b = std::vector{1, 2, 3, 4, 5, 6, 7}; 48 | auto c = std::vector{0, 0, 0, 0, 0}; 49 | 50 | for (auto && [i, j, k] : c9::zip(a, b, c)) { 51 | k = i + j; 52 | } 53 | 54 | auto expected = std::vector{2, 4, 6, 8, 10}; 55 | EXPECT_EQ(c, expected); 56 | } 57 | 58 | 59 | TEST(ZipTuple, ZippedProvidesReference) 60 | { 61 | auto a = std::vector{1, 2, 3, 4, 5, 6}; 62 | auto zipper = c9::zip(a); 63 | 64 | using iter_type = decltype(zipper.begin()); /* zip_iterator<...> */ 65 | using iter_value_type = iter_type::value_type; 66 | using expected_value_type = std::tuple; 67 | 68 | constexpr static auto is_same = std::is_same_v< 69 | iter_value_type, 70 | expected_value_type>; 71 | EXPECT_TRUE(is_same); 72 | } 73 | 74 | 75 | TEST(ZipTuple, ConstZippedProvidesConstReference) 76 | { 77 | const auto a = std::vector{1, 2, 3, 4, 5, 6}; 78 | auto zipper = c9::zip(a); 79 | 80 | using iter_type = decltype(zipper.begin()); /* zip_iterator<...> */ 81 | using iter_value_type = iter_type::value_type; 82 | using expected_value_type = std::tuple; 83 | 84 | constexpr static auto is_same = std::is_same_v< 85 | iter_type::value_type, 86 | expected_value_type>; 87 | EXPECT_TRUE(is_same); 88 | } 89 | 90 | 91 | TEST(ZipTuple, MixedZippedProvidesMixedReference) 92 | { 93 | const auto a = std::vector{1, 2, 3, 4, 5, 6}; 94 | auto b = std::vector{1, 2, 3, 4, 5, 6}; 95 | auto zipper = c9::zip(a, b); 96 | 97 | using iter_type = decltype(zipper.begin()); /* zip_iterator<...> */ 98 | using iter_value_type = iter_type::value_type; 99 | using expected_value_type = std::tuple; 100 | 101 | constexpr static auto is_same = std::is_same_v< 102 | iter_type::value_type, 103 | expected_value_type>; 104 | EXPECT_TRUE(is_same); 105 | } 106 | 107 | 108 | TEST(ZipTuple, CanIterateArray) 109 | { 110 | auto a = std::array{1, 2, 3, 4, 5, 6}; 111 | auto b = std::array{1, 2, 3, 4, 5, 6}; 112 | 113 | auto expected = std::vector{2, 4, 6, 8, 10, 12}; 114 | auto result = std::vector{}; 115 | 116 | for (auto && [i, j] : c9::zip(a, b)) { 117 | result.push_back(i + j); 118 | } 119 | EXPECT_EQ(result, expected); 120 | } 121 | 122 | 123 | TEST(ZipTuple, CanIterateRawArray) 124 | { 125 | int a[] = {1, 2, 3, 4, 5, 6}; 126 | int b[] = {1, 2, 3, 4, 5, 6}; 127 | 128 | auto expected = std::vector{2, 4, 6, 8, 10, 12}; 129 | auto result = std::vector{}; 130 | 131 | for (auto && [i, j] : c9::zip(a, b)) { 132 | result.push_back(i + j); 133 | } 134 | EXPECT_EQ(result, expected); 135 | } 136 | 137 | 138 | TEST(ZipTuple, CanIterateConstRawArray) 139 | { 140 | const int a[] = {1, 2, 3, 4, 5, 6}; 141 | const int b[] = {1, 2, 3, 4, 5, 6}; 142 | 143 | auto expected = std::vector{2, 4, 6, 8, 10, 12}; 144 | auto result = std::vector{}; 145 | 146 | for (auto && [i, j] : c9::zip(a, b)) { 147 | result.push_back(i + j); 148 | } 149 | EXPECT_EQ(result, expected); 150 | } 151 | 152 | 153 | TEST(ZipTuple, CanAssignToVectorOfBool) 154 | { 155 | const auto a = std::vector{1, 0, 1, 0, 1, 0}; 156 | const auto b = std::vector{0, 1, 0, 1, 0, 1}; 157 | 158 | 159 | auto expected = std::vector{1, 1, 1, 1, 1, 1}; 160 | auto result = std::vector{}; 161 | result.resize(a.size()); 162 | 163 | for (auto && [i, j, k] : c9::zip(a, b, result)) { 164 | k = i | j; 165 | } 166 | EXPECT_EQ(result, expected); 167 | } 168 | 169 | 170 | 171 | 172 | template 173 | auto iter_ref(T (&arr)[N]) 174 | { 175 | for (auto && [i] : c9::zip(arr)) { 176 | i = 0; 177 | } 178 | } 179 | 180 | 181 | TEST(ZipTuple, CanIterateRawArrayReference) 182 | { 183 | int raw[6] = {1, 2, 3, 4, 5, 6}; 184 | int expected[6] = {0, 0, 0, 0, 0, 0}; 185 | iter_ref(raw); 186 | EXPECT_EQ(raw[0], 0); 187 | EXPECT_EQ(raw[1], 0); 188 | EXPECT_EQ(raw[2], 0); 189 | EXPECT_EQ(raw[3], 0); 190 | EXPECT_EQ(raw[4], 0); 191 | EXPECT_EQ(raw[5], 0); 192 | } -------------------------------------------------------------------------------- /test_zip_two.cpp: -------------------------------------------------------------------------------- 1 | #include "zip_two.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | #include 10 | 11 | 12 | 13 | TEST(ZipTuple, CanIterateVector) 14 | { 15 | auto a = std::vector{1, 2, 3, 4, 5, 6}; 16 | auto b = std::vector{1, 2, 3, 4, 5, 6}; 17 | 18 | auto expected = std::vector{2, 4, 6, 8, 10, 12}; 19 | auto result = std::vector{}; 20 | 21 | for (auto && [i, j] : c9::zip(a, b)) { 22 | result.push_back(i + j); 23 | } 24 | EXPECT_EQ(result, expected); 25 | } 26 | 27 | 28 | TEST(ZipTuple, CanAssignZippedByReference) 29 | { 30 | auto a = std::vector{1, 2, 3, 4, 5, 6}; 31 | auto b = std::vector{}; 32 | b.resize(a.size()); 33 | 34 | for (auto && [i, j] : c9::zip(a, b)) { 35 | j = i; 36 | } 37 | 38 | EXPECT_EQ(a, b); 39 | } 40 | 41 | 42 | TEST(ZipTuple, WillIterateOverShortestSize) 43 | { 44 | auto a = std::vector{1, 2, 3}; 45 | auto b = std::vector{1, 2, 3, 4, 5, 6, 7}; 46 | 47 | auto count = 0ull; 48 | for (auto && [i, j] : c9::zip(a, b)) { 49 | ++count; 50 | } 51 | 52 | EXPECT_EQ(a.size(), count); 53 | } 54 | 55 | 56 | TEST(ZipTuple, ZippedProvidesReference) 57 | { 58 | auto a = std::vector{1, 2, 3, 4, 5, 6}; 59 | auto zipper = c9::zip(a, a); 60 | 61 | using iter_type = decltype(zipper.begin()); /* zip_iterator<...> */ 62 | using iter_value_type = iter_type::value_type; 63 | using expected_value_type = std::pair; 64 | 65 | constexpr static auto is_same = std::is_same_v< 66 | iter_value_type, 67 | expected_value_type>; 68 | EXPECT_TRUE(is_same); 69 | } 70 | 71 | 72 | TEST(ZipTuple, ConstZippedProvidesConstReference) 73 | { 74 | const auto a = std::vector{1, 2, 3, 4, 5, 6}; 75 | auto zipper = c9::zip(a, a); 76 | 77 | using iter_type = decltype(zipper.begin()); /* zip_iterator<...> */ 78 | using iter_value_type = iter_type::value_type; 79 | using expected_value_type = std::pair; 80 | 81 | constexpr static auto is_same = std::is_same_v< 82 | iter_value_type, 83 | expected_value_type>; 84 | EXPECT_TRUE(is_same); 85 | } 86 | 87 | 88 | TEST(ZipTuple, MixedZippedProvidesMixedReference) 89 | { 90 | const auto a = std::vector{1, 2, 3, 4, 5, 6}; 91 | auto b = std::vector{1, 2, 3, 4, 5, 6}; 92 | auto zipper = c9::zip(a, b); 93 | 94 | using iter_type = decltype(zipper.begin()); /* zip_iterator<...> */ 95 | using iter_value_type = iter_type::value_type; 96 | using expected_value_type = std::pair; 97 | 98 | constexpr static auto is_same = std::is_same_v< 99 | iter_type::value_type, 100 | expected_value_type>; 101 | EXPECT_TRUE(is_same); 102 | } 103 | 104 | 105 | TEST(ZipTuple, CanIterateArray) 106 | { 107 | auto a = std::array{1, 2, 3, 4, 5, 6}; 108 | auto b = std::array{1, 2, 3, 4, 5, 6}; 109 | 110 | auto expected = std::vector{2, 4, 6, 8, 10, 12}; 111 | auto result = std::vector{}; 112 | 113 | for (auto && [i, j] : c9::zip(a, b)) { 114 | result.push_back(i + j); 115 | } 116 | EXPECT_EQ(result, expected); 117 | } 118 | 119 | 120 | TEST(ZipTuple, CanIterateRawArray) 121 | { 122 | int a[] = {1, 2, 3, 4, 5, 6}; 123 | int b[] = {1, 2, 3, 4, 5, 6}; 124 | 125 | auto expected = std::vector{2, 4, 6, 8, 10, 12}; 126 | auto result = std::vector{}; 127 | 128 | for (auto && [i, j] : c9::zip(a, b)) { 129 | result.push_back(i + j); 130 | } 131 | EXPECT_EQ(result, expected); 132 | } 133 | 134 | 135 | TEST(ZipTuple, CanIterateConstRawArray) 136 | { 137 | const int a[] = {1, 2, 3, 4, 5, 6}; 138 | const int b[] = {1, 2, 3, 4, 5, 6}; 139 | 140 | auto expected = std::vector{2, 4, 6, 8, 10, 12}; 141 | auto result = std::vector{}; 142 | 143 | for (auto && [i, j] : c9::zip(a, b)) { 144 | result.push_back(i + j); 145 | } 146 | EXPECT_EQ(result, expected); 147 | } 148 | 149 | 150 | TEST(ZipTuple, CanAssignToVectorOfBool) 151 | { 152 | const auto a = std::vector{1, 0, 1, 0, 1, 0}; 153 | auto b = std::vector{0, 0, 0, 0, 0, 0}; 154 | 155 | for (auto && [i, j] : c9::zip(a, b)) { 156 | j = i; 157 | } 158 | EXPECT_EQ(a, b); 159 | } 160 | -------------------------------------------------------------------------------- /zip_tuple.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | 13 | namespace c9 { 14 | 15 | template 16 | using select_access_type_for = typename std::iterator_traits::reference; 17 | 18 | template 19 | auto any_match_impl(std::tuple const & lhs, 20 | std::tuple const & rhs, 21 | std::index_sequence) -> bool 22 | { 23 | auto result = false; 24 | result = (... || (std::get(lhs) == std::get(rhs))); 25 | return result; 26 | } 27 | 28 | 29 | template 30 | auto any_match(std::tuple const & lhs, std::tuple const & rhs) 31 | -> bool 32 | { 33 | return any_match_impl(lhs, rhs, std::index_sequence_for{}); 34 | } 35 | 36 | 37 | 38 | template 39 | class zip_iterator 40 | { 41 | public: 42 | 43 | using value_type = std::tuple< 44 | select_access_type_for... 45 | >; 46 | 47 | zip_iterator() = delete; 48 | 49 | zip_iterator(Iters && ... iters) 50 | : m_iters {std::forward(iters)...} 51 | { 52 | } 53 | 54 | auto operator++() -> zip_iterator& 55 | { 56 | std::apply([](auto && ... args){ ((args += 1), ...); }, m_iters); 57 | return *this; 58 | } 59 | 60 | auto operator++(int) -> zip_iterator 61 | { 62 | auto tmp = *this; 63 | ++*this; 64 | return tmp; 65 | } 66 | 67 | auto operator!=(zip_iterator const & other) const 68 | { 69 | return !(*this == other); 70 | } 71 | 72 | auto operator==(zip_iterator const & other) const 73 | { 74 | auto result = false; 75 | return any_match(m_iters, other.m_iters); 76 | } 77 | 78 | auto operator*() -> value_type 79 | { 80 | return std::apply([](auto && ... args){ 81 | return value_type(*args...); 82 | }, m_iters); 83 | } 84 | 85 | private: 86 | std::tuple m_iters; 87 | }; 88 | 89 | template 90 | using select_iterator_for = decltype(std::begin(std::declval())); 91 | 92 | 93 | template 94 | class zipper 95 | { 96 | public: 97 | using zip_type = zip_iterator ...>; 98 | 99 | template 100 | zipper(Args && ... args) 101 | : m_args{std::forward(args)...} 102 | { 103 | } 104 | 105 | auto begin() -> zip_type 106 | { 107 | return std::apply([](auto && ... args){ 108 | return zip_type(std::begin(args)...); 109 | }, m_args); 110 | } 111 | auto end() -> zip_type 112 | { 113 | return std::apply([](auto && ... args){ 114 | return zip_type(std::end(args)...); 115 | }, m_args); 116 | } 117 | 118 | private: 119 | std::tuple m_args; 120 | 121 | }; 122 | 123 | 124 | template 125 | auto zip(T && ... t) 126 | { 127 | return zipper{std::forward(t)...}; 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /zip_two.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | /* 7 | This file demonstrates how you can perform parallel iteration of two types. 8 | */ 9 | 10 | 11 | namespace c9 { 12 | 13 | /* This is an example of how you can change the access type for a given type. 14 | Because are example uses std::vector, and std::vector has a specialisation 15 | for `bool` that returns a special iterator that can only be dereferenced by 16 | value, then we need to make sure that, in that case, a value type is 17 | returned rather than a reference. */ 18 | template 19 | using select_access_type_for = std::conditional_t< 20 | std::is_same_v::iterator> || 21 | std::is_same_v::const_iterator>, 22 | typename Iter::value_type, 23 | typename Iter::reference 24 | >; 25 | 26 | 27 | /* This is an iterator-like object. It's not really a proper iterator, but 28 | it does satisfy the requirements needed for range-for. */ 29 | template 30 | class zip_iterator 31 | { 32 | public: 33 | 34 | using value_type = std::pair< 35 | select_access_type_for, 36 | select_access_type_for 37 | >; 38 | 39 | zip_iterator() = delete; 40 | 41 | zip_iterator(Iter1 iter_1_begin, Iter2 iter_2_begin) 42 | : m_iter_1_begin {iter_1_begin} 43 | , m_iter_2_begin {iter_2_begin} 44 | { 45 | } 46 | 47 | auto operator++() -> zip_iterator& 48 | { 49 | ++m_iter_1_begin; 50 | ++m_iter_2_begin; 51 | return *this; 52 | } 53 | 54 | auto operator++(int) -> zip_iterator 55 | { 56 | auto tmp = *this; 57 | ++*this; 58 | return tmp; 59 | } 60 | 61 | auto operator!=(zip_iterator const & other) const 62 | { 63 | return !(*this == other); 64 | } 65 | 66 | 67 | /* The comparison we need to make in order to detect when iteration should 68 | end is when any of the iterators equal their end position. From a 69 | semantics point of view, equality in this sense is not really accurate; 70 | you would expect, ordinarily, that the test would be the aggregate 71 | equivalence of it's members. However, if the things being iterated over 72 | have different sizes, both iterators will never reach the end at the 73 | same time. Therefore we need to return true as soon as any of the 74 | iterators are at their end position, terminating the iteration loop. 75 | */ 76 | auto operator==(zip_iterator const & other) const 77 | { 78 | return 79 | m_iter_1_begin == other.m_iter_1_begin || 80 | m_iter_2_begin == other.m_iter_2_begin; 81 | } 82 | 83 | auto operator*() -> value_type 84 | { 85 | return value_type{*m_iter_1_begin, *m_iter_2_begin}; 86 | } 87 | 88 | private: 89 | Iter1 m_iter_1_begin; 90 | Iter2 m_iter_2_begin; 91 | }; 92 | 93 | 94 | /* We need to select the correct iterator for the passed in types. If one of 95 | the types is a `std::vector const &` then we need to make sure we use 96 | it's `const_iterator, rather than the normal iterator. 97 | std::decay is needed because T is a reference, and is not a complete type. 98 | Using decay will give us the fundamental type and allows to access it's type 99 | definitions */ 100 | template 101 | using select_iterator_for = std::conditional_t< 102 | std::is_const_v>, 103 | typename std::decay_t::const_iterator, 104 | typename std::decay_t::iterator>; 105 | 106 | 107 | /* Class that is called upon to do the zipping. It contains the necessary 108 | functions to satisfy range-for, by providing both begin and end functions. 109 | */ 110 | template 111 | class zipper 112 | { 113 | public: 114 | 115 | using Iter1 = select_iterator_for; 116 | using Iter2 = select_iterator_for; 117 | 118 | using zip_type = zip_iterator; 119 | 120 | template 121 | zipper(V && a, W && b) 122 | : m_a{a} 123 | , m_b{b} 124 | { 125 | } 126 | 127 | auto begin() -> zip_type 128 | { 129 | return zip_type{std::begin(m_a), std::begin(m_b)}; 130 | } 131 | auto end() -> zip_type 132 | { 133 | return zip_type{std::end(m_a), std::end(m_b)}; 134 | } 135 | 136 | private: 137 | T m_a; 138 | U m_b; 139 | }; 140 | 141 | 142 | /* Utility method that used at the user's call site. The reason for this is 143 | that it allows the types of what's passed in to be deduced. Otherwise if 144 | you were to use the class directly, you would need to provide template 145 | arguments. 146 | We also need to take a universal reference -- not because we want to take 147 | both lvalues and rvalues (it doesn't make sense to take an rvalue) but 148 | because we don't know whether the type is const or non-const. */ 149 | template 150 | auto zip(T && t, U && u) 151 | { 152 | return zipper{std::forward(t), std::forward(u)}; 153 | } 154 | 155 | } // namespace c9 156 | --------------------------------------------------------------------------------