├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── range.hpp └── test.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012–2020 Konrad Rudolph 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CXXFLAGS = -std=c++11 -pedantic -Wall -Wextra -Werror 2 | CC = ${CXX} 3 | 4 | test: test.o 5 | 6 | test.o: test.cpp range.hpp 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Re-imagining the `for` loop 2 | 3 | C++11 now knows two distinct types of `for` loops: the classic loop over an “index” and the range-based `for` loop which vastly simplifies the iteration over a range specified by a pair of iterators. 4 | 5 | By contrast, Python knows only one loop type – roughly equivalent to the range-based for loop. In fact, loops over indices are exceedingly rare, but made possible by the use of the `range` method: 6 | 7 | ```python 8 | for i in range(10): 9 | print i 10 | ``` 11 | 12 | Which does what it promises – although Python version < 3.0 does the “wrong” thing and actually instantiates the whole collection in memory at once; a remedy is `xrange` which yields values lazily as they are consumed by the loop. 13 | 14 | C++11 effortlessly allows the same but there is no standard library function to provide this. [Boost.Range][] provides *part* of the functionality via `irange` which only works on integers, and not for unlimited ranges (this will make sense in a second). 15 | 16 | The header `range.hpp` provides a very basic implementation for this. It allows running the following code: 17 | 18 | ```c++ 19 | using util::lang::range; 20 | 21 | for (auto i : range(1, 5)) 22 | cout << i << "\n"; 23 | 24 | for (auto u : range(0u)) 25 | if (u == 3u) break; 26 | else cout << u << "\n"; 27 | 28 | for (auto c : range('a', 'd')) 29 | cout << c << "\n"; 30 | 31 | for (auto i : range(100).step(-3)) 32 | if (i < 90) break; 33 | else cout << i << "\n"; 34 | ``` 35 | 36 | `range` with a single argument deviates from the Python semantic and creates an endless loop, unless it’s interrupted manually. This is an interesting use-case that cannot be modelled in Python using `range`. 37 | 38 | ## Iterating over container indices 39 | 40 | In Python, the one-argument version of `range` is often used to iterate over the indices of a container via `range(len(container))`. Because that overload creates an infinite range in our C++ library, we cannot use this idiom. 41 | 42 | But we can do better anyway. For those few cases where we actually want to iterate over a container’s indices, we just use the `indices` function: 43 | 44 | ```c++ 45 | using util::lang::indices; 46 | 47 | std::vector x{1, 2, 3}; 48 | for (auto i : indices(x)) 49 | cout << i << '\n'; 50 | ``` 51 | 52 | This works as expected for *any* type which has a member function `size() const` that returns some integral type. It also works with `initializer_list`s and C-style fixed-size arrays.[1](#f1) 53 | 54 | Adding `.step(…)` to the end of either `range` or `indices` specifies a step size instead of the default, 1. 55 | 56 | The construct works for arbitrary types which fulfil the interface requirements (incrementing, copying, equality comparison, default construction in the case of infinite ranges). 57 | 58 | **1** This includes string literals, which are C-style strings that include null termination; this may lead to surprising results, because `indices("test")` results in 0, 1, 2, 3, 4, whereas `indices(std::string{"test"})` results in 0, 1, 2, 3. [↩](#a1) 59 | 60 | ## Performance (the cost of beauty) 61 | 62 | When compiling with optimisations enabled (and why wouldn’t you?), using the `range` function yield very similar output compared with a manual `for` loop. In fact, on g++ 4.8 with `-O2` or higher, *the following two loops yield identical assembly*. 63 | 64 | ```c++ 65 | for (int i = 0; i < n; ++i) 66 | cout << i; 67 | 68 | for (int i : range(0, n)) 69 | cout << i; 70 | ``` 71 | 72 | Even though the `range` function creates a proxy container and an iterator wrapper, those are completely elided from the resulting code. 73 | 74 | **☞ Beauty is free.** 75 | 76 | [Boost.Range]: http://www.boost.org/doc/libs/1_54_0/libs/range/doc/html/index.html 77 | -------------------------------------------------------------------------------- /range.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTIL_LANG_RANGE_HPP 2 | #define UTIL_LANG_RANGE_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace util { namespace lang { 9 | 10 | namespace detail { 11 | 12 | template 13 | struct range_iter_base : std::iterator { 14 | range_iter_base(T current) : current(current) { } 15 | 16 | T operator *() const { return current; } 17 | 18 | T const* operator ->() const { return ¤t; } 19 | 20 | range_iter_base& operator ++() { 21 | ++current; 22 | return *this; 23 | } 24 | 25 | range_iter_base operator ++(int) { 26 | auto copy = *this; 27 | ++*this; 28 | return copy; 29 | } 30 | 31 | bool operator ==(range_iter_base const& other) const { 32 | return current == other.current; 33 | } 34 | 35 | bool operator !=(range_iter_base const& other) const { 36 | return not (*this == other); 37 | } 38 | 39 | protected: 40 | T current; 41 | }; 42 | 43 | } // namespace detail 44 | 45 | template 46 | struct step_range_proxy { 47 | struct iterator : detail::range_iter_base { 48 | iterator(T current, T step) 49 | : detail::range_iter_base(current), step_(step) { } 50 | 51 | using detail::range_iter_base::current; 52 | 53 | iterator& operator ++() { 54 | current += step_; 55 | return *this; 56 | } 57 | 58 | iterator operator ++(int) { 59 | auto copy = *this; 60 | ++*this; 61 | return copy; 62 | } 63 | 64 | // Loses commutativity. Iterator-based ranges are simply broken. :-( 65 | bool operator ==(iterator const& other) const { 66 | return step_ > 0 ? current >= other.current 67 | : current < other.current; 68 | } 69 | 70 | bool operator !=(iterator const& other) const { 71 | return not (*this == other); 72 | } 73 | 74 | T step_; 75 | }; 76 | 77 | step_range_proxy(T begin, T end, T step) 78 | : begin_(begin, step), end_(end, step) { } 79 | 80 | iterator begin() const { return begin_; } 81 | 82 | iterator end() const { return end_; } 83 | 84 | std::size_t size() const { 85 | if (*end_ >= *begin_) { 86 | // Increasing and empty range 87 | if (begin_.step_ < T{0}) return 0; 88 | } else { 89 | // Decreasing range 90 | if (begin_.step_ > T{0}) return 0; 91 | } 92 | return std::ceil(std::abs(static_cast(*end_ - *begin_) / begin_.step_)); 93 | } 94 | 95 | private: 96 | iterator begin_; 97 | iterator end_; 98 | }; 99 | 100 | template 101 | struct range_proxy { 102 | struct iterator : detail::range_iter_base { 103 | iterator(T current) : detail::range_iter_base(current) { } 104 | }; 105 | 106 | range_proxy(T begin, T end) : begin_(begin), end_(end) { } 107 | 108 | step_range_proxy step(T step) { 109 | return {*begin_, *end_, step}; 110 | } 111 | 112 | iterator begin() const { return begin_; } 113 | 114 | iterator end() const { return end_; } 115 | 116 | std::size_t size() const { return *end_ - *begin_; } 117 | 118 | private: 119 | iterator begin_; 120 | iterator end_; 121 | }; 122 | 123 | template 124 | struct step_inf_range_proxy { 125 | struct iterator : detail::range_iter_base { 126 | iterator(T current = T(), T step = T()) 127 | : detail::range_iter_base(current), step(step) { } 128 | 129 | using detail::range_iter_base::current; 130 | 131 | iterator& operator ++() { 132 | current += step; 133 | return *this; 134 | } 135 | 136 | iterator operator ++(int) { 137 | auto copy = *this; 138 | ++*this; 139 | return copy; 140 | } 141 | 142 | bool operator ==(iterator const&) const { return false; } 143 | 144 | bool operator !=(iterator const&) const { return true; } 145 | 146 | private: 147 | T step; 148 | }; 149 | 150 | step_inf_range_proxy(T begin, T step) : begin_(begin, step) { } 151 | 152 | iterator begin() const { return begin_; } 153 | 154 | iterator end() const { return iterator(); } 155 | 156 | private: 157 | iterator begin_; 158 | }; 159 | 160 | template 161 | struct infinite_range_proxy { 162 | struct iterator : detail::range_iter_base { 163 | iterator(T current = T()) : detail::range_iter_base(current) { } 164 | 165 | bool operator ==(iterator const&) const { return false; } 166 | 167 | bool operator !=(iterator const&) const { return true; } 168 | }; 169 | 170 | infinite_range_proxy(T begin) : begin_(begin) { } 171 | 172 | step_inf_range_proxy step(T step) { 173 | return {*begin_, step}; 174 | } 175 | 176 | iterator begin() const { return begin_; } 177 | 178 | iterator end() const { return iterator(); } 179 | 180 | private: 181 | iterator begin_; 182 | }; 183 | 184 | template 185 | auto range(T begin, U end) -> range_proxy::type> { 186 | using C = typename std::common_type::type; 187 | return {static_cast(begin), static_cast(end)}; 188 | } 189 | 190 | template 191 | infinite_range_proxy range(T begin) { 192 | return {begin}; 193 | } 194 | 195 | namespace traits { 196 | 197 | template 198 | struct has_size { 199 | template 200 | static auto check(T*) -> 201 | typename std::is_integral< 202 | decltype(std::declval().size())>::type; 203 | 204 | template 205 | static auto check(...) -> std::false_type; 206 | 207 | using type = decltype(check(0)); 208 | static constexpr bool value = type::value; 209 | }; 210 | 211 | } // namespace traits 212 | 213 | template ::value>> 214 | auto indices(C const& cont) -> range_proxy { 215 | return {0, cont.size()}; 216 | } 217 | 218 | template 219 | range_proxy indices(T (&)[N]) { 220 | return {0, N}; 221 | } 222 | 223 | template 224 | range_proxy::size_type> 225 | indices(std::initializer_list&& cont) { 226 | return {0, cont.size()}; 227 | } 228 | 229 | } } // namespace util::lang 230 | 231 | #endif // ndef UTIL_LANG_RANGE_HPP 232 | -------------------------------------------------------------------------------- /test.cpp: -------------------------------------------------------------------------------- 1 | #include "range.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | template 9 | void print_range(R const& range); 10 | 11 | template 12 | void test_range_size(R const& range); 13 | 14 | int main() { 15 | using std::cout; 16 | using util::lang::range; 17 | using util::lang::indices; 18 | 19 | cout << "Basic usage: iterating over a range of numbers.\n"; 20 | for (auto i : range(1, 5)) { 21 | cout << i << " "; 22 | } 23 | cout << "\n"; 24 | 25 | cout << "Ranges can be “infinite”.\n"; 26 | for (auto u : range(0u)) { 27 | if (u == 3u) { 28 | cout << "\n"; 29 | break; 30 | } 31 | cout << u << " "; 32 | } 33 | 34 | cout << "Ranges can be non-numeric, as long as the type is incrementable and equality comparable.\n"; 35 | print_range(range('a', 'd')); 36 | 37 | cout << "Ranges can be non-contiguous.\n"; 38 | print_range(range(20u, 29u).step(2u)); 39 | 40 | cout << "… and we can even step backwards.\n"; 41 | for (auto i : range(100).step(-3)) { 42 | if (i < 90) { 43 | cout << "\n"; 44 | break; 45 | } 46 | cout << i << " "; 47 | } 48 | cout << "\n"; 49 | 50 | cout << "Container indices are a special case of ranges.\n"; 51 | std::vector x{1, 2, 3}; 52 | print_range(indices(x)); 53 | 54 | print_range(indices({"foo", "bar"})); 55 | 56 | cout << "Strings are containers, too.\n"; 57 | print_range(indices("foobar").step(2)); 58 | cout << "\n"; 59 | 60 | // TODO: Test cases; do something smarter with them. 61 | print_range(range(6, 10)); 62 | print_range(range(1, 8).step(2)); 63 | print_range(range(8, 1).step(-2)); 64 | print_range(range(8.0, 1.0).step(-2.0)); 65 | cout << "\n"; 66 | 67 | cout << "Mixed type inference:\n"; 68 | print_range(range(0, sizeof "Hello")); 69 | cout << "Inferred as mangled type name " 70 | << typeid(typename std::iterator_traits::value_type).name() 71 | << " (expected: " << typeid(decltype(sizeof "Hello")).name() << ")\n"; 72 | cout << "\n"; 73 | 74 | test_range_size(range(1, 8).step(2)); 75 | test_range_size(range(8.0, 1.0).step(-2.0)); 76 | test_range_size(range(8, 1).step(-2)); 77 | test_range_size(range(0.1, 0.11).step(2)); 78 | test_range_size(range(-7, 1).step(7)); 79 | } 80 | 81 | namespace util { namespace lang { 82 | 83 | template 84 | std::ostream& operator <<(std::ostream& out, step_range_proxy const& r) { 85 | return out << "range(" << *r.begin() << ", " << *r.end() << ")" 86 | << ".step(" << r.begin().step_ << ")"; 87 | } 88 | 89 | template 90 | std::ostream& operator <<(std::ostream& out, range_proxy const& r) { 91 | return out << "range(" << *r.begin() << ", " << *r.end() << ")"; 92 | } 93 | 94 | }} 95 | 96 | template 97 | void print_range(R const& range) { 98 | using T = typename std::iterator_traits::value_type; 99 | std::cout << range << " = "; 100 | std::copy(range.begin(), range.end(), std::ostream_iterator(std::cout, " ")); 101 | std::cout << "\n"; 102 | } 103 | 104 | template 105 | std::size_t manual_range_size(R const& range) { 106 | std::size_t size = 0; 107 | for (auto const& _ : range) ++size, (void) _; 108 | return size; 109 | } 110 | 111 | template 112 | void test_range_size(R const& range) { 113 | auto const real_size = manual_range_size(R{range}); 114 | if (real_size == range.size()) { 115 | std::cout << range << ".size() = " << real_size << "\n"; 116 | } else { 117 | std::cout << "ERROR: " << range << ".size() ≠ " << real_size 118 | << " (was " << range.size() << ")!\n"; 119 | } 120 | } 121 | --------------------------------------------------------------------------------