注意
WandBoxのclangではうまく動かないようです(コメント参照)
目標
Pythonのfor i in range(n)
と同等のものをC++の範囲ベースfor
を用いてfor(auto i:range(n))
と書けるようにします.つまり,次のforループと同じ動作をするものです.
for(int i = 0; i < n ; ++i)
方針
あらかじめ0,1,...,n-1が格納された配列等を用意しておけば,比較的簡単に実現できるのですが,この方法では$O(n)$のメモリと実行時間が必要となります.たとえforループが早期に中断された場合でも,これらのコストが発生します。そこで,必要に応じてlazyに生成する方法を,なるべく簡潔に実現します.
イテレータを作る
例えば,下の範囲ベースのforを実行すると,0からn-1が表示されるようにします.
for(const auto i: range(n))
std::cout << i << "\n";
同様の動作をするイテレータベースのfor
は次のように書くことができます.
for (auto it = range(n).begin(); it != range(n).end(); ++it)
std::cout << *it << "\n";
大雑把に言えば,このfor
が正しく動作するように,range
クラスを設計すれば,範囲ベースのfor
も機能します.そのためには,以下のメンバ関数や演算子のオーバーロードを設定します.
-
begin()
:最初の要素を指すイテレータ -
end()
:最後の要素を指すイテレータ -
&operator*()
: ポインタ演算子*
で,イテレータが指す先の値を返す -
operator!=
: 不等価演算子!=
で,イテレータが一致するかどうかを返す -
&operator++
: インクリメント演算子++
で,次の要素を指すようにイテレータを更新する
通常,begin()
等は,イテレータを返すべきなのですが,あえて無視して,イテレータベースのfor
が動くようにすればいいという方針で,簡潔に設計します.
ソースコード
range
クラスを定義します.
- メンバ変数
n
:ここに格納する値で,値自身とイテレータの両方を表します. - コンストラクタ
range(int n)
:引数n
でメンバ変数n
を初期化します. -
auto begin()
:range(0)
を返すので,it = range(n).begin()
の場合,n
の値に依存せず,it
にrange(0)
が代入されます. -
auto end()
:インスタンス自身を返すので,range(n).end()
はrange(n)
を返します. -
int &operator*()
:メンバ変数n
の値を返します.よって,*it
は,it
が持つrange
のインスタンスのメンバ変数n
の値となります. -
bool operator!=
:2つのrange
のインスタンスが一致するかどうか判定します.よって,it != range(n).end()
はit
がrange(n)
ではない場合に,true
となります. -
range &operator++()
:range
のインスタンスのn
の値を1増やし,インスタンス自身を返します.したがって,++it
では,range
のインスタンスit
がもつn
の値が1増えます.
class range {
int n;
public:
range(int n) : n(n) {}
auto begin() { return range(0); }
auto end() { return *this; }
int &operator*() { return n; }
bool operator!=(const range &r) { return n != r.n; }
range &operator++() {
++n;
return *this;
}
};
イテレータベースのfor
を使って,0から9までを表示するプログラムは次のようになります.
int main() {
for (auto it = range(10).begin(); it != range(10).end(); ++it)
std::cout << *it << "\n";
}
範囲ベースのfor
では次のように簡潔に書くことができます.
int main() {
for(const auto i:range(10))
std::cout << i << "\n";
}
実行結果は次のようになり,意図した通り動作しています.
0
1
2
3
4
5
6
7
8
9
ChatGPT-4に聞いてみた
ちなみに,ChatGPT-4にこのコードの添削を求めると,次のようにダメ出しされました.
ChatGPT: 提供されたコードにはいくつかの誤解や誤りが含まれています。これらを修正して、意図された挙動を正しく達成するためには次のような変更が必要です。
誤りとその修正
-
begin() と end() メソッドの誤使用:
現在のbegin() が range(0) を返していますが、これは単に n = 0 の新しい range オブジェクトを作成します。これでは、イテレータとして機能しません。 end() も同様に range(n) を返していますが、これはイテレータではなく、範囲の終わりを示す値としてのみ意味を持ちます。 -
range クラスがイテレータの責務も担っている:
クラス range がコンテナの役割とイテレータの役割の両方を担っています。これは設計として混乱を招く可能性があります。イテレータは別のクラスとして定義するべきです。
以下に、上記の問題を解決し、範囲ベースの for ループで利用できるように修正したコードを示します。
#include <iostream>
class range {
public:
class iterator {
int pos;
public:
iterator(int pos) : pos(pos) {}
bool operator!=(const iterator& other) const { return pos != other.pos; }
int operator*() const { return pos; }
iterator& operator++() { ++pos; return *this; }
};
range(int n) : begin_(0), end_(n) {}
iterator begin() const { return iterator(begin_); }
iterator end() const { return iterator(end_); }
private:
int begin_, end_;
};
int main() {
for (auto i : range(10)) {
std::cout << i << "\n";
}
return 0;
}
このコードでは、range クラス内に iterator サブクラスが定義されています。begin() と end() メソッドは、それぞれ開始位置と終了位置を示す iterator インスタンスを返します。この設計により、範囲ベースの for ループを自然に使うことができ、Python の for i in range(n) に近い形での使用が可能になります。
おわりに
ChatGPTはメンバ変数を3つ使っていますが,ここで紹介したイテレータを無視する実装方法では,使用するメンバ変数は1つだけです.
また,C++20
であればstd::views::iota
を使って簡単に書けるようです.