HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppMinor

Yet another Pythonic range implementation for C++

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
pythonicrangeyetanotherforimplementation

Problem

I recently had the need to loop from zero to some limit only known at runtime. Instead of writing:

for(int i = 0; i < limit; ++i)
{
    // Some repetitive thing
}


I wanted to write something similar to what I often use in Python and D:

for i in range(0, limit):
    # Some repetitive thing

foreach(i; 0 .. limit)
{
    // Some repetitive thing
}


So I ended up with the following:

```
#include

namespace detail
{
template
class basic_range
{
public:
explicit basic_range(T const last, int const step = 1)
: basic_range(T{ 0 }, last, step)
{}

explicit basic_range(T const first, T const last, int const step = 1)
: first{ first, last, step }, last{ last, last, step }
{}

basic_range(basic_range const& other) = delete;
basic_range(basic_range && other) = default;
basic_range operator=(basic_range const& other) = delete;
basic_range operator=(basic_range && other) = delete;

public:
struct iterator : std::iterator
{
explicit iterator(T const from, T const to, int const step = T{ 1 })
: from{ from }, to{ to }, step{ step }
{}

iterator(iterator const& other) = default;
iterator(iterator && other) = delete;
iterator operator=(iterator const& other) = delete;
iterator operator=(iterator && other) = delete;

T const operator*() const { return from; }

bool operator==(iterator const& other) const { return from == other.from; }
bool operator!=(iterator const& other) const { return from != other.from; }

void operator++()
{
from += step;
check_limit();
}

private:
void check_limit()
{
if (step > 0)
{
if (from > to)
{
from =

Solution

So I came up with a solution that should be as fast a "normal" loop. As already discussed your main problem is the check_limits function that is called on every iteration.

I removed the check (see Change 4 comments) and instead added a function to calculate a range that is compatible with the size of step (see Change 1 comments).

This function is now used instead of just using T last (see Change 2 comments).

Also: To prevent someone from messing around with the iterator I made the constructor private and added basic_range as friend class (see Change 3 comments).

#include 

namespace detail
{
    template
    class basic_range
    {
    private:

        //Change 1: Calculate end
        static T adjustedLast(T const first, T const last, T const step)
        {
            //Using modulo on signed types is UB
            using UT = typename std::make_unsigned::type;
            UT difference = std::abs(last - first);
            UT stepping = std::abs(step);

            T underflow = T(difference % stepping) * (step / T(stepping));
            if (underflow == 0) return last;

            return last + (step - underflow);
        }

    public:
        explicit basic_range(T const last, T const step = 1)
            : basic_range(T{ 0 }, last, step)
        {}

        explicit basic_range(T const first, T const last, T const step = 1)
            : first{ first, adjustedLast(first, last, step), step }, last{ adjustedLast(first, last, step), adjustedLast(first, last, step), step } //Change 2: use calculated last
        {}

        basic_range(basic_range const& other) = delete;
        basic_range(basic_range && other) = default;
        basic_range operator=(basic_range const& other) = delete;
        basic_range operator=(basic_range && other) = delete;

    public:
        struct iterator : std::iterator
        {
            //Change 3: Make constructor private
            friend class basic_range;

        private: //Change 3: Make constructor private
            explicit iterator(T const from, T const to, int const step = T{ 1 })
                : from{ from }, to{ to }, step{ step }
            {
            }

        public:
            iterator(iterator const& other) = default;
            iterator(iterator && other) = delete;
            iterator operator=(iterator const& other) = delete;
            iterator operator=(iterator && other) = delete;

            T const operator*() const { return from; }

            bool operator==(iterator const& other) const { return from == other.from; }
            bool operator!=(iterator const& other) const { return from != other.from; }

            void operator++()
            {
                from += step;
                //Change 4: Remove check()
            }

            //Change 4: Remove check()

        private:
            T         from;
            T   const to;
            int const step;
        };

        typedef iterator       iterator;
        typedef iterator const const_iterator;

        const_iterator begin() const { return first; }
        const_iterator end()   const { return last; }

    private:
        const_iterator first;
        const_iterator last;
    };

    template::value >
    struct get_integral_type
    {
        typedef std::underlying_type_t type;
    };

    template
    struct get_integral_type
    {
        typedef T type;
    };

    template::value >
    using get_integral_type_t = typename get_integral_type::type;
}

template
auto range(T const begin, T const end, int const step = 1)
{
    typedef detail::get_integral_type_t type;

    static_assert(std::is_integral::value,
        "Only integer-based types allowed!");

    return detail::basic_range{
        static_cast(begin),
            static_cast(end),
            step
    };
}

template
auto range(T const begin, U const end, int const step = 1)
{
    typedef std::common_type_t
        ,
        detail::get_integral_type_t
        > type;

    static_assert(std::is_integral::value,
        "Only integer-based types allowed!");

    return detail::basic_range{
        static_cast(begin),
            static_cast(end),
            step
    };
}

template
auto reverse_range(T const from, T const to, int const step = -1)
{
    return range(from, to, step);
}

template
auto reverse_range(T const from, U const to, int const step = -1)
{
    return range(from, to, step);
}

#include 

int main()
{
    for (auto const i : range(0, -100, -3))
    {
        std::cout << i << std::endl;
    }
}


The new function adjustedLast might seem a little bit difficult in the beginning. All it does is to calculate the next step equal or greater than the current last in the direction of step:

```
//Change 1: Calculate end
static T adjustedLast(T const first, T const last, T const step)
{
//Using modulo on signed types is UB
using UT = typename std::make_unsigned::type;
UT difference = std::abs(last - first);
UT stepping = std:

Code Snippets

#include <iterator>

namespace detail
{
    template< typename T >
    class basic_range
    {
    private:

        //Change 1: Calculate end
        static T adjustedLast(T const first, T const last, T const step)
        {
            //Using modulo on signed types is UB
            using UT = typename std::make_unsigned<T>::type;
            UT difference = std::abs(last - first);
            UT stepping = std::abs(step);

            T underflow = T(difference % stepping) * (step / T(stepping));
            if (underflow == 0) return last;

            return last + (step - underflow);
        }

    public:
        explicit basic_range(T const last, T const step = 1)
            : basic_range(T{ 0 }, last, step)
        {}

        explicit basic_range(T const first, T const last, T const step = 1)
            : first{ first, adjustedLast(first, last, step), step }, last{ adjustedLast(first, last, step), adjustedLast(first, last, step), step } //Change 2: use calculated last
        {}

        basic_range(basic_range const& other) = delete;
        basic_range(basic_range && other) = default;
        basic_range operator=(basic_range const& other) = delete;
        basic_range operator=(basic_range && other) = delete;

    public:
        struct iterator : std::iterator< std::forward_iterator_tag, T >
        {
            //Change 3: Make constructor private
            friend class basic_range<T>;

        private: //Change 3: Make constructor private
            explicit iterator(T const from, T const to, int const step = T{ 1 })
                : from{ from }, to{ to }, step{ step }
            {
            }

        public:
            iterator(iterator const& other) = default;
            iterator(iterator && other) = delete;
            iterator operator=(iterator const& other) = delete;
            iterator operator=(iterator && other) = delete;

            T const operator*() const { return from; }

            bool operator==(iterator const& other) const { return from == other.from; }
            bool operator!=(iterator const& other) const { return from != other.from; }

            void operator++()
            {
                from += step;
                //Change 4: Remove check()
            }


            //Change 4: Remove check()

        private:
            T         from;
            T   const to;
            int const step;
        };

        typedef iterator       iterator;
        typedef iterator const const_iterator;

        const_iterator begin() const { return first; }
        const_iterator end()   const { return last; }

    private:
        const_iterator first;
        const_iterator last;
    };

    template< typename T, bool is_enum = std::is_enum< T >::value >
    struct get_integral_type
    {
        typedef std::underlying_type_t< T > type;
    };

    template< typename T >
    struct get_integral_type< T, false >
    {
        typedef T type;
    };

    template< typename T, bool is_enum = 
//Change 1: Calculate end
static T adjustedLast(T const first, T const last, T const step)
{
    //Using modulo on signed types is UB
    using UT = typename std::make_unsigned<T>::type;
    UT difference = std::abs(last - first);
    UT stepping = std::abs(step);

    T underflow= T(difference % stepping) * (step / T(stepping));
    if (underflow== 0) return last;

    return last + (step - underflow);
}
std::ctype<char>::do_widen(char) const:
        mov     eax, esi
        ret
main:
        push    r12
        push    rbp
        xor     ebp, ebp
        push    rbx
        jmp     .L6
.L14:
        movsx   esi, BYTE PTR [rbx+67]
.L5:
        mov     rdi, r12
        add     ebp, 1
        call    std::basic_ostream<char, std::char_traits<char> >::put(char)
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >::flush()
        cmp     ebp, 100
        je      .L12
.L6:
        mov     esi, ebp
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        mov     r12, rax
        mov     rax, QWORD PTR [rax]
        mov     rax, QWORD PTR [rax-24]
        mov     rbx, QWORD PTR [r12+240+rax]
        test    rbx, rbx
        je      .L13
        cmp     BYTE PTR [rbx+56], 0
        jne     .L14
        mov     rdi, rbx
        call    std::ctype<char>::_M_widen_init() const
        mov     rax, QWORD PTR [rbx]
        mov     esi, 10
        mov     rax, QWORD PTR [rax+48]
        cmp     rax, OFFSET FLAT:std::ctype<char>::do_widen(char) const
        je      .L5
        mov     rdi, rbx
        call    rax
        movsx   esi, al
        jmp     .L5
.L12:
        pop     rbx
        xor     eax, eax
        pop     rbp
        pop     r12
        ret
.L13:
        call    std::__throw_bad_cast()
        sub     rsp, 8
        mov     edi, OFFSET FLAT:std::__ioinit
        call    std::ios_base::Init::Init()
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:std::__ioinit
        mov     edi, OFFSET FLAT:std::ios_base::Init::~Init()
        add     rsp, 8
        jmp     __cxa_atexit
std::ctype<char>::do_widen(char) const:
        mov     eax, esi
        ret
main:
        push    r12
        push    rbp
        xor     ebp, ebp
        push    rbx
        jmp     .L6
.L14:
        movsx   esi, BYTE PTR [rbx+67]
.L5:
        mov     rdi, r12
        add     ebp, 1
        call    std::basic_ostream<char, std::char_traits<char> >::put(char)
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >::flush()
        cmp     ebp, 100
        je      .L12
.L6:
        mov     esi, ebp
        mov     edi, OFFSET FLAT:std::cout
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        mov     r12, rax
        mov     rax, QWORD PTR [rax]
        mov     rax, QWORD PTR [rax-24]
        mov     rbx, QWORD PTR [r12+240+rax]
        test    rbx, rbx
        je      .L13
        cmp     BYTE PTR [rbx+56], 0
        jne     .L14
        mov     rdi, rbx
        call    std::ctype<char>::_M_widen_init() const
        mov     rax, QWORD PTR [rbx]
        mov     esi, 10
        mov     rax, QWORD PTR [rax+48]
        cmp     rax, OFFSET FLAT:std::ctype<char>::do_widen(char) const
        je      .L5
        mov     rdi, rbx
        call    rax
        movsx   esi, al
        jmp     .L5
.L12:
        pop     rbx
        xor     eax, eax
        pop     rbp
        pop     r12
        ret
.L13:
        call    std::__throw_bad_cast()
        sub     rsp, 8
        mov     edi, OFFSET FLAT:std::__ioinit
        call    std::ios_base::Init::Init()
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:std::__ioinit
        mov     edi, OFFSET FLAT:std::ios_base::Init::~Init()
        add     rsp, 8
        jmp     __cxa_atexit

Context

StackExchange Code Review Q#120858, answer score: 5

Revisions (0)

No revisions yet.