2
0

C++でPythonのrange(n)をなるべく簡潔に実現

Last updated at Posted at 2024-05-07

注意

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の値に依存せず,itrange(0)が代入されます.
  • auto end():インスタンス自身を返すので,range(n).end()range(n)を返します.
  • int &operator*():メンバ変数nの値を返します.よって,*itは,itが持つrangeのインスタンスのメンバ変数nの値となります.
  • bool operator!=:2つのrangeのインスタンスが一致するかどうか判定します.よって,it != range(n).end()itrange(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: 提供されたコードにはいくつかの誤解や誤りが含まれています。これらを修正して、意図された挙動を正しく達成するためには次のような変更が必要です。

誤りとその修正

  1. begin() と end() メソッドの誤使用:
    現在のbegin()range(0) を返していますが、これは単に n = 0 の新しい range オブジェクトを作成します。これでは、イテレータとして機能しません。 end() も同様に range(n) を返していますが、これはイテレータではなく、範囲の終わりを示す値としてのみ意味を持ちます。
  2. 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 ループを自然に使うことができ、Pythonfor i in range(n) に近い形での使用が可能になります。

おわりに

ChatGPTはメンバ変数を3つ使っていますが,ここで紹介したイテレータを無視する実装方法では,使用するメンバ変数は1つだけです.
また,C++20であればstd::views::iotaを使って簡単に書けるようです.

2
0
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0