I came up with a C++17 (I cannot use C++ >= 20 in my project) implementation of a python-like range function (class).
The Code
"range.hpp"
#include <stdexcept>
namespace stdx
{
namespace details
{
template<typename T>
struct RangeIterator {
constexpr RangeIterator(T cursor, T stop, T step)
: _cursor{cursor}
, _stop{stop}
, _step{step}
{}
constexpr RangeIterator& operator++() noexcept
{
_cursor += _step;
return *this;
}
constexpr RangeIterator operator++(int) noexcept
{
auto iter_cpy = *this;
++*this;
return iter_cpy;
}
// The (... || ...) below is mandatory because the order of iterator
// comparison is not clear. Client code may call
// * for (auto it = range.begin(); it != range.end(); ++it)
// * for (auto it = range.begin(); range.end() != it; ++it)
// Also a simple check for _cursor != other._cursor is insufficient
// as the step may be not in {-1, +1}.
[[nodiscard]] constexpr bool operator!=(const RangeIterator &other) const noexcept
{
return _step > 0 ? (_cursor < other._stop || other._cursor < _stop)
: (_cursor > other._stop || other._cursor > _stop);
}
[[nodiscard]] constexpr T operator*() const noexcept
{
return _cursor;
}
T _cursor;
T _stop;
T _step;
};
};
template<typename T>
class Range
{
public:
explicit constexpr Range(T stop)
: _start{}
, _stop{stop}
, _step{1}
{}
constexpr Range(T start, T stop, T step = 1)
: _start{start}
, _stop{stop}
, _step{step}
{
if (step == T{})
throw std::runtime_error{"stdx::Range step must be != 0"};
}
[[nodiscard]] constexpr details::RangeIterator<T> begin() const noexcept
{
return details::RangeIterator{_start, _stop, _step};
}
[[nodiscard]] constexpr details::RangeIterator<T> end() const noexcept
{
return details::RangeIterator{_stop, _stop, _step};
}
private:
T _start;
T _stop;
T _step;
};
};
Example usage
"main.cpp"
#include <iostream>
#include "range.hpp"
int main() {
for (auto n : stdx::Range{5})
std::cout << n << "\n";
for (auto n : stdx::Range{5, 0, -2})
std::cout << n << "\n";
auto rng = stdx::Range{5, 0, -2};
for (auto it = rng.begin(); it != rng.end(); ++it)
std::cout << *it << "\n";
return 0;
}
GTEST
#include <gtest/gtest.h>
#include "gmock/gmock.h"
#include "range.hpp"
constexpr auto cRange = stdx::Range{0, 10, 2};
template<typename T>
constexpr int sumRange(const stdx::Range<T>& rng) {
T s{};
for (auto i : rng) { s += i; }
return s;
}
static_assert(sumRange(cRange) == 20, "Sum must be 20");
namespace
{
template<typename T>
std::vector<T> createVec(const stdx::Range<T>& range) {
std::vector<T> result;
for (const auto n : range) {
result.push_back(n);
}
return result;
}
} // end of anonymous namespace
TEST(RangeTest, Basic) {
auto rangeVec{createVec(stdx::Range{5})};
EXPECT_EQ(5, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(0, 1, 2, 3, 4));
}
TEST(RangeTest, BasicEmpty) {
auto rangeVec{createVec(stdx::Range{0})};
EXPECT_EQ(0, rangeVec.size());
}
TEST(RangeTest, StartStop) {
auto rangeVec{createVec(stdx::Range{-2, 3})};
EXPECT_EQ(5, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(-2, -1, 0, 1, 2));
rangeVec = createVec(stdx::Range{0, 5});
EXPECT_EQ(5, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(0, 1, 2, 3, 4));
rangeVec = createVec(stdx::Range{2, 7});
EXPECT_EQ(5, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(2, 3, 4, 5, 6));
}
TEST(RangeTest, StartStopEmpty) {
auto rangeVec{createVec(stdx::Range{42, 42})};
EXPECT_EQ(0, rangeVec.size());
rangeVec = createVec(stdx::Range{43, 42});
EXPECT_EQ(0, rangeVec.size());
}
TEST(RangeTest, StartStopStepForward) {
auto rangeVec{createVec(stdx::Range{-2, 3, 1})};
EXPECT_EQ(5, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(-2, -1, 0, 1, 2));
rangeVec = createVec(stdx::Range{0, 5, 1});
EXPECT_EQ(5, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(0, 1, 2, 3, 4));
rangeVec = createVec(stdx::Range{2, 7, 1});
EXPECT_EQ(5, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(2, 3, 4, 5, 6));
rangeVec = createVec(stdx::Range{-3, 3, 1});
EXPECT_EQ(6, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(-3, -2, -1, 0, 1, 2));
rangeVec = createVec(stdx::Range{-1, 5, 1});
EXPECT_EQ(6, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(-1, 0, 1, 2, 3, 4));
rangeVec = createVec(stdx::Range{1, 7, 1});
EXPECT_EQ(6, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(1, 2, 3, 4, 5, 6));
rangeVec = createVec(stdx::Range{-2, 3, 2});
EXPECT_EQ(3, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(-2, 0, 2));
rangeVec = createVec(stdx::Range{0, 5, 2});
EXPECT_EQ(3, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(0, 2, 4));
rangeVec = createVec(stdx::Range{2, 7, 2});
EXPECT_EQ(3, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(2, 4, 6));
rangeVec = createVec(stdx::Range{-3, 3, 2});
EXPECT_EQ(3, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(-3, -1, 1));
rangeVec = createVec(stdx::Range{-1, 5, 2});
EXPECT_EQ(3, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(-1, 1, 3));
rangeVec = createVec(stdx::Range{1, 7, 2});
EXPECT_EQ(3, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(1, 3, 5));
}
TEST(RangeTest, StartStopStepBackward) {
auto rangeVec{createVec(stdx::Range{3, -2, -1})};
EXPECT_EQ(5, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(3, 2, 1, 0, -1));
rangeVec = createVec(stdx::Range{5, 0, -1});
EXPECT_EQ(5, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(5, 4, 3, 2, 1));
rangeVec = createVec(stdx::Range{7, 2, -1});
EXPECT_EQ(5, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(7, 6, 5, 4, 3));
rangeVec = createVec(stdx::Range{3, -3, -1});
EXPECT_EQ(6, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(3, 2, 1, 0, -1, -2));
rangeVec = createVec(stdx::Range{5, -1, -1});
EXPECT_EQ(6, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(5, 4, 3, 2, 1, 0));
rangeVec = createVec(stdx::Range{7, 1, -1});
EXPECT_EQ(6, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(7, 6, 5, 4, 3, 2));
rangeVec = createVec(stdx::Range{3, -2, -2});
EXPECT_EQ(3, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(3, 1, -1));
rangeVec = createVec(stdx::Range{5, 0, -2});
EXPECT_EQ(3, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(5, 3, 1));
rangeVec = createVec(stdx::Range{7, 2, -2});
EXPECT_EQ(3, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(7, 5, 3));
rangeVec = createVec(stdx::Range{3, -3, -2});
EXPECT_EQ(3, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(3, 1, -1));
rangeVec = createVec(stdx::Range{5, -1, -2});
EXPECT_EQ(3, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(5, 3, 1));
rangeVec = createVec(stdx::Range{7, 1, -2});
EXPECT_EQ(3, rangeVec.size());
EXPECT_THAT(rangeVec, testing::ElementsAre(7, 5, 3));
}
TEST(RangeTest, StartStopStepEmpty) {
auto rangeVec{createVec(stdx::Range{42, 42, 1})};
EXPECT_EQ(0, rangeVec.size());
rangeVec = createVec(stdx::Range{43, 42, 1});
EXPECT_EQ(0, rangeVec.size());
rangeVec = createVec(stdx::Range{42, 42, -1});
EXPECT_EQ(0, rangeVec.size());
rangeVec = createVec(stdx::Range{42, 43, -1});
EXPECT_EQ(0, rangeVec.size());
}
TEST(RangeTest, byIteratorForward) {
auto range{stdx::Range{3, -3, -1}};
auto rangeVec{createVec(range)};
std::vector<int> x;
for (auto it = range.begin(); it != range.end(); ++it)
{
x.push_back(*it);
}
EXPECT_EQ(rangeVec, x);
x.clear();
for (auto it = range.begin(); it != range.end(); it++)
{
x.push_back(*it);
}
EXPECT_EQ(rangeVec, x);
x.clear();
for (auto it = range.begin(); range.end() != it; it++)
{
x.push_back(*it);
}
EXPECT_EQ(rangeVec, x);
x.clear();
for (auto it = range.begin(); range.end() != it; ++it)
{
x.push_back(*it);
}
EXPECT_EQ(rangeVec, x);
range = stdx::Range{-3, 2, 2};
rangeVec = createVec(range);
x.clear();
for (auto it = range.begin(); it != range.end(); ++it)
{
x.push_back(*it);
}
EXPECT_EQ(rangeVec, x);
x.clear();
for (auto it = range.begin(); it != range.end(); it++)
{
x.push_back(*it);
}
EXPECT_EQ(rangeVec, x);
x.clear();
for (auto it = range.begin(); range.end() != it; it++)
{
x.push_back(*it);
}
EXPECT_EQ(rangeVec, x);
x.clear();
for (auto it = range.begin(); range.end() != it; ++it)
{
x.push_back(*it);
}
EXPECT_EQ(rangeVec, x);
}
TEST(RangeTest, byIteratorBackward) {
auto range{stdx::Range{3, -3, -1}};
auto rangeVec{createVec(range)};
std::vector<int> x;
for (auto it = range.begin(); it != range.end(); ++it)
{
x.push_back(*it);
}
EXPECT_EQ(rangeVec, x);
x.clear();
for (auto it = range.begin(); it != range.end(); it++)
{
x.push_back(*it);
}
EXPECT_EQ(rangeVec, x);
x.clear();
for (auto it = range.begin(); range.end() != it; it++)
{
x.push_back(*it);
}
EXPECT_EQ(rangeVec, x);
x.clear();
for (auto it = range.begin(); range.end() != it; ++it)
{
x.push_back(*it);
}
EXPECT_EQ(rangeVec, x);
range = stdx::Range{2, -3, -2};
rangeVec = createVec(range);
x.clear();
for (auto it = range.begin(); it != range.end(); ++it)
{
x.push_back(*it);
}
EXPECT_EQ(rangeVec, x);
x.clear();
for (auto it = range.begin(); it != range.end(); it++)
{
x.push_back(*it);
}
EXPECT_EQ(rangeVec, x);
x.clear();
for (auto it = range.begin(); range.end() != it; it++)
{
x.push_back(*it);
}
EXPECT_EQ(rangeVec, x);
x.clear();
for (auto it = range.begin(); range.end() != it; ++it)
{
x.push_back(*it);
}
EXPECT_EQ(rangeVec, x);
}
Issues
- The RangeIterator class operator "!=" needs to perform three comparisons. One could possibly refactor the "_step > 0" comparison into a template parameter but I am not sure if it is worth it.
- Currently, all three constructor arguments are of template parameter type. This sounds sensible at first but it makes writing some code ugly. E.g.
std::vector<int> v{1, 2, 3, 4};
for (auto n : stdx::Range{0, v.size(), -1})
std::cout << n << "\n";
will not compile as -1 is deduced as int and v.size() as size_t, of course. One would have to static_cast the v.size() which kind of defeats the purpose of writing short and concise code, imho. Maybe it is an idea to make the "step" parameter to be an int32_t and drop support for floating point iteration. I am not sure if this is a usecase at all?
Review Request
I would like to have this code reviewed for general feedback, maybe including a statement or two concerning the issues I raised.
std::ranges::iotafor inspiration? Even though it's not available to you at present, you might be able to migrate one day. And it helps readers if your code uses a similar interface. \$\endgroup\$views::iota. \$\endgroup\$