インデックスforループを単純化する:範囲ベースのバージョン

運命の意志により、たまたまPythonスクリプトを使用して1つの自動化タスクを実行しました。 基本的な構造を研究して、次のコードが私の最大の関心を呼び起こしました。



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





便利で読みやすく、簡潔(ファッショナブル、スタイリッシュ、若者)! 同じループをC ++で整理してみませんか? それから来たもの-カットの下。



最初の試行-マクロ



マクロの欠陥について多くのことが書かれています。 そして、主なルールは次のとおりです。「マクロを使用せずに何かを実装できる場合は、それを実行してください。」 ただし、マクロを使用することが正当な場合もあります。

マクロは、非標準の構成で言語を拡張するためによく使用されます。たとえば、コードを読みやすくするために、永遠のループキーワードを入力するために使用されます。



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





ちなみに、非標準サイクルの実装についても質問しました。 マクロを使用してこのビジネスを実装しようとするとどうなりますか。 このようなもの:



 #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; }
      
      





もちろん、そのようなコードはそのタスクを果たしますが、目標は達成されているだけではありません。コードを読みやすく簡潔にするのではなく、さらに混乱させます。



さらに、他にも多くの欠点があります。



そして、私が言えること-これはPythonの例とはまったく異なります。



2回目の試み-コレクションジェネレーター関数



Pythonのrange()の ドキュメントを注意深く読むと、 range()が一度に範囲からすべての値のリストを生成することがわかります。 まったく同じことを行い、各要素がインデックス値であるstd :: vectorを返す関数を作成します。



 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); }
      
      





C ++ 11標準のコレクション値を反復処理するための新しい構文を考えると、次のコードを書くことができます。



 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; }
      
      





Vootoot、私たちが達成したいことのように見えます。 「0〜10の範囲のすべてのiについて」と表示されます。 同意します。「iが0から、10未満、1ずつ増加する」よりも良い音です。 その結果、プログラムの出力は次のようになります。



[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]





このソリューションには、定義から明らかな欠点があります-この操作のためのリソースの過剰な消費。 また、値の範囲が広いほど、中間体が消費するリソースが多くなります。 Pythonでは、この問題を解決するために、値をその場で生成できるxrange()関数があります。



残念ながら、ジェネレーター関数は利用できないため、別のソリューションを探す必要があります。



3回目の試行、最終-疑似コレクション



カスタムコレクションクラスが範囲ベースのループでの通過をサポートするために、もう何もする必要はありません-イテレータをコレクションの最初と最後にそれぞれ返すbegin()およびend()関数を実装します。 さらに、イテレータ自体のクラスを実装する必要があります。 しかし、インターフェイスレベルでのみコレクションとなるクラスを実装し、内部実装ではすべての値を保存せず、必要に応じて値を生成するとどうなりますか?



クラスの単純化された実装は次のようになります。



 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; };
      
      





格納する必要があるのは、範囲の境界とステップだけです。 その後、単純な算術を使用して範囲の任意の要素を取得できます( 演算子[]を参照)。 主な作業はイテレータクラスに割り当てられます。



 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; };
      
      





equals()関数の存在をさらに明確にする価値があると思います。 非整数の範囲があり、たとえば0から10まで0.1の増分であるとします。 反復子の比較は、各反復子に格納されている範囲の現在の値を比較することに基づいています。 しかし、C ++で浮動小数点数を比較することはまったく不可能です。 ここで読むことができる理由をもっと読んでください 。 「真正面から」を比較すると、サイクルは無限大になる可能性が高いと言えます。 最善の方法は、差を許容絶対誤差と比較することです。 これは、 equals()関数で実装されます。 さらに、この場合、絶対誤差は範囲ステップです。



これで、オーバーヘッドに多くを費やすことなく、必要な形式でサイクルを記述することが実際に可能になりました。



コードのフルバージョン:

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; };
      
      










All Articles