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 <iterator>
namespace detail
{
template< typename T >
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< std::forward_iterator_tag, T >
{
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 = to;
}
}
else
{
if (from < to)
{
from = to;
}
}
}
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 = std::is_enum< T >::value >
using get_integral_type_t = typename get_integral_type< T >::type;
}
With some supporting functions to aid in constructing a range:
template< typename T >
auto range(T const begin, T const end, int const step = 1)
{
typedef detail::get_integral_type_t< T > type;
static_assert(std::is_integral< type >::value,
"Only integer-based types allowed!");
return detail::basic_range< type >{
static_cast<type>(begin),
static_cast<type>(end),
step
};
}
template< typename T, typename U >
auto range(T const begin, U const end, int const step = 1)
{
typedef std::common_type_t
<
detail::get_integral_type_t< T >,
detail::get_integral_type_t< U >
> type;
static_assert(std::is_integral< type >::value,
"Only integer-based types allowed!");
return detail::basic_range< type >{
static_cast<type>(begin),
static_cast<type>(end),
step
};
}
template< typename T >
auto reverse_range(T const from, T const to, int const step = -1)
{
return range(from, to, step);
}
template< typename T, typename U >
auto reverse_range(T const from, U const to, int const step = -1)
{
return range(from, to, step);
}
This allows for syntax very close to the Python version:
for(auto const i: range(0, limit))
{
// Some repetitive task
}
I am, so far, pleased with how well it works both forwards and backwards, however, I feel that the iterator implementation is sloppy and, since documentation on correct iterator implementation is hard to come by, I turn to the community to help me further refine this design. Of course, you are free to pick holes elsewhere. For example, I should probably check the inputs to the range functions before blindly returning an object that will misbehave...
A more complete example of usage can be found here which includes some simple unit tests to help debug the code. Any insights are appreciated
std::cout
differs in only 6 instructions to the "normal" for loop. \$\endgroup\$ – Simon Kraemer Feb 23 '16 at 13:03for(auto i: range(0, 1000000)
againstfor(int i = 0; i < 1000000; ++i)
my solution runs 0.1s slower :(. Whether or not that is significant depends on the application, of course. The point here, is that my solution (at least on Visual Studio 2015) is slower than the traditional for loop. \$\endgroup\$ – BigDaveDev Feb 23 '16 at 13:16range(0, limit)
wherelimit
is of, sayunsigned int
type then the template resolution fails for the first instance of range, but matches the second, since 0 isint
.std::common_type_t
then helps to adjust the types to the most compatible and create abasic_range
. So yes, I'm afraid there was a use case. @SimonKraemer good idea about aligninglast
to step. That brings basic_range to within 0.05s of the standard for loop :) \$\endgroup\$ – BigDaveDev Feb 23 '16 at 14:19