Python’s range() in 20 lines of C++

The Python language does not have an iteration-based for-loop in the style of C and C++; instead it uses a Python list (or tuple) in conjunction with the keywords for and in. Python 2 uses the helper function range() to create a suitable list, while Python 3 implements this function as an iterator class (to avoid a potential delay before the first iteration of the loop due to populating a very large list). If we were to implement this in C++, we could populate a std::vector and iterate over that, but this could potentially suffer from the space-and-time overheads of the Python 2 version. So instead we’ll choose to use iterators in the style of the STL containers (actually, if you know your Standard Library iterator categories, it’s forward iterators we want to emulate, however I haven’t used the actual iterator types available because of only wanting to be compatible with range-for, not the standard containers).

Head over to C++ now, and compare Modern C++’s range-for…

for ( auto a: pyrange{/* params */} ) { /* note pyrange is a class name */ }

…with the version that’s always existed and which shows its implementation (okay, please forgive use of C++11’s auto)…

auto r = pyrange{/* params */};
for ( auto i = r.begin(); i != r.end(); ++i ) {
    auto a = *i;
    /* loop body uses 'a' */

It should be clear from these code fragments that our pyrange{} class needs two member functions: begin() and end(). The iterator object i returned by begin() needs three operators defined for it: compare with end(), prefix increment and dereference. (C++17 and later allows end() to return a different type to that returned by begin() in order to be compatible with range-for.)

The type of code we want to write is as follows:

for (auto i : pyrange{10}) cout << i << ' '; // "0 1 2 3 4 5 6 7 8 9"
for (auto j : pyrange<long long>{20, 0, -1}) cout << j << ' ';
    // "20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1"
for (auto k : pyrange{-1, -5}) cout << k << ' '; // (no output)

On to the implementation. I’ve used structs to implement the pyrange::begin() and pyrange::end() return types, and private member variables which hold these values. The begin iterator contains a starting value and a step, the end iterator just an end value. The implementation of the operators of the begin iterator class should not present any surprises, as it is incremented, dereferenced and compared with the end iterator.

The two pyrange{} constructors cater for one, two or three parameters, the types of which are used as the type parameter for the class, due to declaring the constructors explicit. If only one parameter is provided this is the end value of the range, otherwise the parameters are start, end, step (step defaults to plus one). Invalid combinations create an “empty” range, as occurs with Python; this means that an infinite loop cannot occur (setting the step to zero causes an exception to be thrown).

// pyrange.hpp : implement Python-style range class to use with range-for statement

#include <stdexcept>

template<typename T = int>
class pyrange {
    struct end_range {
        T e;
    struct begin_range {
        T b, s;
        begin_range& operator++() { b += s; return *this; }
        bool operator!=(const end_range& c) const
            { return (s < 0) ? b > c.e : b < c.e; }
        const T& operator*() const { return b; }
    begin_range br;
    end_range er;
    explicit pyrange(T arg_e) : br{ 0, 1 }, er{ arg_e } {}
    explicit pyrange(T arg_b, T arg_e, T arg_s = 1) : br{ arg_b, arg_s }, er{ arg_e }
    { if (!arg_s) throw std::out_of_range{"pyrange: step must be non-zero"}; }
    auto& begin() { return br; }
    auto& end() { return er; }

So there you have it, Python-esque ranges in C++! (By the way don’t confuse this with the C++ Ranges TS which is part of C++20; this is the reason I’ve called the class pyrange and not range.) Keen students of the code will realise that it defines half-open ranges (where the last value is not included). Should you require closed ranges (where the last value is included, which is unlike Python’s behavior), simply change line 14 to the following:

            { return (s < 0) ? b >= c.e : b <= c.e; }

If you want to use this code (or a modified version) in your own projects, feel free to do so, as it’s MIT Licensed.

Resources: Download or browse the source code

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s