📜 ⬆️ ⬇️

Simplify the for-cycle index: range-based version

By the will of fate, I had the opportunity to tackle one task of automation using a Python script. Studying the basic constructions, I was most interested in the following code:

for index in range(0,10) : do_stuff() 

Convenient, readable, succinctly (fashionable, stylish, youth)! Why not arrange the same cycle in C ++? What came of it - under the cut.

Attempt the first - macros


Much has been written about the disadvantages of macros. And the main rule says: "If you can implement something without using macros, do so." But sometimes the use of macros is justified.
Macros are often used to extend a language with non-standard constructions — for example, to enter a perpetual loop keyword for more readable code:

 #define infinite_loop while(true) infinite_loop { do_stuff(); } 

By the way, we also wondered about the implementation of a non-standard cycle. What if you try to implement this business using macros. Like this:
')
 #include <iostream> #define ranged_for(var, min, max, step) for(auto var = (min); var < (max); var += (step) ) int main() { ranged_for(i, 0, 10, 1) { std::cout << i << std::endl; } return 0; } 

Of course, this code performs its task, but the goal is more than not achieved - instead of making the code more readable and concise, we have rather confused it more.

In addition, there are several other disadvantages:

And what can I say - it is absolutely not similar to the example from Python.

Attempt two - collection function generator


If you carefully read the range () documentation from Python, you can see that range () generates a list of all values ​​in the range at once. We will do the same and write a function that will return std :: vector where each element is an index value:

 template<typename T> std::vector<T> range(T min, T max, T step) { const bool is_unsigned = std::is_unsigned<T>::value; if (is_unsigned && min > max) return std::vector<T>(0); size_t size = size_t((max - min) / step); if (!is_unsigned && size < 0) return std::vector<T>(); if (size == 0) return std::vector<T>(1, min); std::vector<T> values; values.reserve(size); if (step < 0) { for (T i = min; i > max; i += step) { values.push_back(i); } } else { for (T i = min; i < max; i += step) { values.push_back(i); } } return values; } template<typename T> std::vector<T> range(T min, T max) { return range<T>(min, max, 1); } template<typename T> std::vector<T> range(T max) { return range<T>(0, max); } 

Given the new syntax for enumerating collection values ​​in the C ++ 11 standard, it is possible to write the following code:

 int main() { std::cout << '['; for (int i : range<int>(10)) std::cout << i << ' '; std::cout << ']' << std::endl; std::cout << '['; for (int i : range<int>(0, 10)) std::cout << i << ' '; std::cout << ']' << std::endl; std::cout << '['; for (int i : range<int>(0, 10, 2)) std::cout << i << ' '; std::cout << ']' << std::endl; std::cout << '['; for (int i : range<int>(10, 2)) std::cout << i << ' '; std::cout << ']' << std::endl; std::cout << '['; for (int i : range<int>(10, 2, -1)) std::cout << i << ' '; std::cout << ']' << std::endl; return 0; } 

Vooot, this is already similar to what we want to achieve. Now it reads "Over all i in the range from 0 to 10". Agree, it sounds better than “From i to 0, while less than 10, increase by 1”. As a result, the output of the program will be as follows:

[0 1 2 3 4 5 6 7 8 9]
[0 1 2 3 4 5 6 7 8 9]
[0 2 4 6 8]
[]
[10 9 8 7 6 5 4 3]


This solution has an obvious disadvantage, which follows from the definition - excessive consumption of resources for this operation. And the greater the range of values, the more resources the intermediate link consumes. In Python, to solve this problem, there is an xrange () function that allows you to generate values ​​on the fly.

Unfortunately, function generators are not available to us, so we will have to look for another solution.

Attempt third, final - pseudo-collection


In order for a custom collection class to support passage using range-based loops , there is nothing to do — implement the begin () and end () functions that return iterators to the beginning and end of the collection, respectively. Additionally, you must implement the class of the iterator itself. But what if you implement a class that the collection will be only at the interface level, but the internal implementation will not store all the values, but will generate them as necessary?

Then a simplified implementation of our class might look like this:

 template<typename T> class range sealed { public: range(T _min, T _max, T _step = T(1)) : m_min(_min), m_max(_max), m_step(_step) { } T operator[](size_t index) { return (m_min + index * m_step); } size_t size() { return static_cast<size_type>((m_max - m_min) / m_step); } range_iterator<range<T>> begin() { return range_iterator<range<T>>(this, m_min); } range_iterator<range<T>> end() { return range_iterator<range<T>>(this, m_min + size() * m_step); } private: T m_min; T m_max; T m_step; }; 

All that needs to be stored is the boundaries of the range and pitch. Then any element of the range can be obtained using simple arithmetic (see operator [] ). The main work is assigned to the iterator class:

 template<typename T> class range_iterator sealed { public: typedef T range_type; typedef range_iterator<range_type> self_type; typedef typename range_type::value_type value_type; range_iterator(const range_type* const range, value_type start_value) : m_range(range), m_value(start_value) { } operator value_type() const { return m_value; } value_type& operator*() { return m_value; } self_type& operator++() { m_value += m_range->step(); return *this; } self_type operator++(int) { self_type tmp(*this); ++(*this); return tmp; } bool operator==(const self_type& other) const { return ((m_range == other.m_range) && (equals<value_type>(m_value, other.m_value, m_range->step()))); } bool operator!=(const self_type& other) const { return !((*this) == other); } private: template<typename R> static bool equals(R a, R b, R e) { return a == b; } template<> static bool equals(double a, double b, double e) { return std::abs(a - b) < std::abs(e); } template<> static bool equals(float a, float b, float e) { return std::abs(a - b) < std::abs(e); } const range_type* const m_range; value_type m_value; }; 

I think in addition it is necessary to clarify the presence of the equals () function. Suppose we have a non-integer range, and, say, from 0 to 10 in increments of 0.1. Comparing iterators is based on comparing current values ​​from a range stored in each of them. But comparing floating point numbers in C ++ is just not possible. More details why you can read here . Let me just say that if you compare "head on", then most likely the cycle will be infinite. The best way is to compare the difference with the permissible absolute error. This is implemented in the equals () function. In this case, the absolute error is the step of the range.

Now we can really write the cycle in the form we need and at the same time do not spend much on overhead.

Full version of the code:
range.h
 template<typename T> class range_iterator : std::iterator<std::random_access_iterator_tag, typename T::value_type> { public: typedef T range_type; typedef range_iterator<range_type> self_type; typedef std::random_access_iterator_tag iterator_category; typedef typename range_type::value_type value_type; typedef typename range_type::size_type size_type; typedef typename range_type::difference_type difference_type; typedef typename range_type::pointer pointer; typedef typename range_type::const_pointer const_pointer; typedef typename range_type::reference reference; typedef typename range_type::const_reference const_reference; range_iterator(const range_type* const range, value_type start_value) : m_range(range), m_value(start_value) { } range_iterator(const self_type&) = default; range_iterator(self_type&&) = default; range_iterator& operator=(const range_iterator&) = default; ~range_iterator() = default; operator value_type() const { return m_value; } value_type& operator*() { return m_value; } self_type& operator++() { m_value += m_range->step(); return *this; } self_type operator++(int) { self_type tmp(*this); ++(*this); return tmp; } self_type& operator--() { m_value -= m_range->step(); return *this; } self_type operator--(int) { self_type tmp(*this); --(*this); return tmp; } self_type operator+(difference_type n) { self_type tmp(*this); tmp.m_value += m_range->step() * n; return tmp; } self_type& operator+=(difference_type n) { m_value += n * m_range->step(); return (*this); } self_type operator-(difference_type n) { self_type tmp(*this); tmp.m_value -= n * m_range->step(); return tmp; } self_type& operator-=(difference_type n) { m_value -= n * m_range->step(); return (*this); } bool operator==(const self_type& other) const { return ((m_range == other.m_range) && (equals<value_type>(m_value, other.m_value, m_range->step()))); } bool operator!=(const self_type& other) const { return !((*this) == other); } private: template<typename T> static bool equals(T a, T b, T e) { return a == b; } template<> static bool equals(double a, double b, double e) { return std::abs(a - b) < std::abs(e); } template<> static bool equals(float a, float b, float e) { return std::abs(a - b) < std::abs(e); } const range_type* const m_range; value_type m_value; }; template<typename T> class range sealed { static_assert(std::is_arithmetic<T>::value, "Template type should be a integral-type"); public: typedef T value_type; typedef T* pointer; typedef const T* const_pointer; typedef T& reference; typedef const T& const_reference; typedef size_t size_type; typedef ptrdiff_t difference_type; typedef range<value_type> self_type; typedef class range_iterator<self_type> iterator; typedef std::reverse_iterator<iterator> reverse_iterator; range(value_type _min, value_type _max, value_type _step = value_type(1)) : m_min(_min), m_max(_max), m_step(_step) { if (m_step == 0) { throw std::invalid_argument("Step equals zero"); } } range(const self_type&) = default; range(self_type&&) = default; range& operator=(const self_type&) = default; ~range() = default; bool operator==(const self_type& _obj) const { return (m_max == _obj.max()) && (m_min == _obj.min()) && (m_step == _obj.step()); } bool operator!=(const self_type& _obj) const { return !(this == _obj); } value_type operator[](size_type _index) const { #ifdef _DEBUG if (_index > size()) { throw std::out_of_range("Index out-of-range"); } #endif return (m_min + (_index * m_step)); } bool empty() const { bool is_empty = ((m_max < m_min) && (m_step > 0)); is_empty |= ((m_max > m_min) && (m_step < 0)); return is_empty; } size_type size() const { if (empty()) { return 0; } return static_cast<size_type>((m_max - m_min) / m_step); } value_type min() const { return m_min; } value_type max() const { return m_max; } value_type step() const { return m_step; } iterator begin() const { iterator start_iterator(this, m_min); return start_iterator; } iterator end() const { iterator end_iterator(this, m_min + size() * m_step); return end_iterator; } reverse_iterator rbegin() const { reverse_iterator start_iterator(end()); return start_iterator; } reverse_iterator rend() const { reverse_iterator end_iterator(begin()); return end_iterator; } private: value_type m_min; value_type m_max; value_type m_step; }; 


Source: https://habr.com/ru/post/264803/


All Articles