Python風Rangeの実装 in C++

範囲forについて学んだので実装した。

普通の for

STLコンテナはイテレーターを持つので、要素を順番に取る処理がfor文で書ける。

#include <iostream>
#include <set>
#include <string>
#include <vector>

int main() {
  std::vector<int> vec{1, 2, 3};
  // begin()とend()でイテレーターを取得し、for文の条件として使う
  // itrを定義するときの型はautoと書くのが普通
  for (std::vector<int>::iterator itr = vec.begin(); itr != vec.end(); ++itr) {
    // イテレーターはコンテナの成分のポインタっぽく使える
    std::cout << *itr << std::endl;
  }
  std::cout << std::endl;

  // vectorだけでなく他のコンテナでもほとんど同じように書ける
  std::set<std::string> set{"a", "b", "c"};
  for (std::set<std::string>::iterator itr = set.begin(); itr != set.end(); ++itr) {
    std::cout << *itr << std::endl;
  }
}

範囲 for

C++11では同じ意味のfor文を次のように書ける。

#include <iostream>
#include <set>
#include <string>
#include <vector>

int main() {
  std::vector<int> vec{1, 2, 3};
  // 範囲 for
  for (int i : vec) {
    std::cout << i << std::endl;
  }
  std::cout << std::endl;

  std::set<std::string> set{"a", "b", "c"};
  // strを参照にしてstd::stringのコピーが起こらないようにする
  // さらにconstをつけてループ内でsetの内容が変わらないことを保証する
  for (const std::string &str : set) {
    std::cout << str << std::endl;
  }
}
1
2
3

a
b
c

pythonのrange

pythonには range() 関数があり、以下のように使える。

for i in range(3, 10, 2):
  print(i)
3
5
7
9

これをC++で実装する。

範囲 for の展開

リファレンスによると、

for ( range_declaration : range_expression )
  loop_statement

は以下のように展開される。

{
  auto && __range = range_expression;

  for (auto __begin = begin(__range), __end = end(__range); __begin != __end; ++__begin) {
    range_declaration = *__begin;
    loop_statement
  }
}

範囲 for に対応したクラス

展開式を見ると、クラスを範囲 for に対応させるには適当な型のついた

  • begin()
  • end()
  • operator!=
  • operator++
  • operator*

があればいいことが分かる。

C++で実装

とりあえず最低限のものを実装する。

#include <iostream>
#include <vector>
#include <cassert>
#include <cmath>

template <typename T>
class Range {
 private:
  T m_value;        // 現在値
  const T m_end;    // 終了値
  const T m_stride; // ループ一回ごとに進む値

 public:
  // コンストラクタ
  Range(T begin, T end, T stride)
      : m_value(begin), m_end(end), m_stride(stride) {}
  // ゲッタ
  const T &value() const { return m_value; }
  // begin()の戻り値がforループの中で編集される
  Range<T> begin() const { return *this; }
  // end()の返り値との比較によってループから抜けるかどうかが判定される
  // begin()とend()は同じ型を持つ必要がある
  const Range<T> end() const { return Range(m_end, m_end, m_end); }
  // operator!=(rhs) はforループから抜けてよいかどうかを返す
  // Rangeオブジェクトが異なるかどうかではない
  bool operator!=(const Range<T> &rhs) const {
    return m_stride > 0 ? m_value < rhs.value() : m_value > rhs.value();
  }
  // m_valueを進める
  void operator++() { m_value += m_stride; }
  // 現在の値を返す
  const T &operator*() const { return m_value; }
};


// 実際に使う
int main() {
  int sum = 0;
  for (int i : Range<int>{0, 20, 3}) {
    sum += i; // i = 0,3,6,9,12,15,18
  }
  assert(sum == 63);


// Rangeの使い回し
  float sumd = 0;
  const Range<double> r{0.1, 5.3, 1.1};
  for (double d : r) {
    for (double d : r) {
      sumd += d;
    }
    sumd -= d * 5;
  }
  assert(fabs(sumd) < 1.0e-5);

  std::cout << "OK";
  return 0;
}
OK

とりあえずはこれで使える。

改善

この実装にはいくつか問題がある。

  • 同じRangeオブジェクトのend()は常に同じ値しか返さないので、const参照を返したほうがいい。
  • Rangeオブジェクトを作るたびに型を明示的に書かなければいけないのは面倒くさい。

これを直す。

#include <iostream>
#include <vector>
#include <cassert>
#include <cmath>

template <typename T>
class Range {
 private:
  // Rangeとは別にクラスを作って、begin(), end()はこのクラスのオブジェクトを返すことにする
  class Iterator {
   // 変数/メソッドは同じ
   private:
    T m_value;
    const T m_end;
    const T m_stride;

   public:
    Iterator(T begin, T end, T stride)
        : m_value(begin), m_end(end), m_stride(stride) {}
    const T &value() { return m_value; }
    void operator++() { m_value += m_stride; }
    T operator*() const { return m_value; }
    bool operator!=(const Iterator &end) const {
      return m_stride > 0 ? m_value < end.m_end : m_value > end.m_end;
    }

  };

 private:
  // Rangeが begin(), end() で返すのは
  // 常に同じIteratorオブジェクトなのでどちらもconstで問題ない
  const Iterator m_begin, m_end;

 public:
  Range(T begin, T end, T stride)
      : m_begin{begin, end, stride}, m_end{end, end, end} {}

  // begin() の戻り値は常に同じだが、for文の中で変更されるので値で帰す
  Iterator begin() const { return m_begin; }
  // end() の返り値は常に同じでfor文中で変更されることもないのでconst参照で返す
  const Iterator &end() const { return m_end; }

};

// 関数テンプレートを使って型の明示を不要にする
template<typename T>
Range<T> range(T begin, T end, T stride) {
  return {begin, end, stride};
}

// 実際に使う (一つ前のプログラムと同じ)
int main() {
  int sum = 0;
  for (int i : range(0, 20, 3)) {
    sum += i; // i = 0,3,6,9,12,15,18
  }
  assert(sum == 63);

  // Rangeの使い回し
  float sumd = 0;
  auto r = range(0.1, 5.3, 1.1);
  for (double d : r) {
    for (double d : r) {
      sumd += d;
    }
    sumd -= d * 5;
  }
  assert(fabs(sumd) < 1.0e-5);

  std::cout << "OK";
  return 0;
}
OK

だいぶpythonっぽくできる。 range() の引数を省略できるようにするのは簡単なので省略。

C++17

C++17 では範囲forの展開方法が変わり、begin()とend()の返り値の型が一致しなくても良くなっている。具体的には、C++11では

{
    auto && __range = range_expression ;
    for (auto __begin = begin_expr, __end = end_expr;
            __begin != __end; ++__begin) {
        range_declaration = *__begin;
        loop_statement
    }
}

だったものが

{
    auto && __range = range_expression ;
    auto __begin = begin_expr ;
    auto __end = end_expr ;
    for ( ; __begin != __end; ++__begin) {
        range_declaration = *__begin;
        loop_statement
    }
}

に変わっている(リファレンス)。

そのため、python風のrangeも簡潔に書ける。

#include <iostream>
#include <vector>
#include <cassert>
#include <cmath>

// python風range (C++17)
template <typename T>
class Range {
 private:
  T m_value;
  const T m_end;
  const T m_stride;

 public:
  Range(T begin, T end, T stride)
      :  m_value(begin), m_end(end), m_stride(stride) {}
  const T &value() const { return m_value; }
  Range<T> begin() const { return *this; }
  // end()の返り値の型が変わる
  T end() const { return m_end; }
  // operator!=の引数の型が変わる
  bool operator!=(const T &value) const {
    return m_stride > 0 ? m_value < value : m_value > value;
  }
  void operator++() { m_value += m_stride; }
  const T &operator*() const { return m_value; }
};

template<typename T>
Range<T> range(T begin, T end, T stride) {
  return {begin, end, stride};
}

// 実際に使う
int main() {
  int sum = 0;
  for (int i : range(0, 20, 3)) {
    sum += i;
  }
  assert(sum == 63);

  // Rangeの使い回し
  float sumd = 0;
  auto r = range(0.1, 5.3, 1.1);
  for (double d : r) {
    for (double d : r) {
      sumd += d;
    }
    sumd -= d * 5;
  }
  assert(fabs(sumd) < 1.0e-5);

  std::cout << "OK";
  return 0;
}
OK