问题
I want to create a range
-like construct in c++, that will be used like this:
for (auto i: range(5,9))
cout << i << ' '; // prints 5 6 7 8
for (auto i: range(5.1,9.2))
cout << i << ' '; // prints 5.1 6.1 7.1 8.1 9.1
Handling the integer case is relatively easy:
template<typename T>
struct range
{
T from, to;
range(T from, T to) : from(from), to(to) {}
struct iterator
{
T current;
T operator*() { return current; }
iterator& operator++()
{
++current;
return *this;
}
bool operator==(const iterator& other) { return current == other.current; }
bool operator!=(const iterator& other) { return current != other.current; }
};
iterator begin() const { return iterator{ from }; }
iterator end() const { return iterator{ to }; }
};
However, this does not work in the float
case, since the standard range-based loop in C++
checks whether iter==end
and not whether iter <= end
as you would do in a for a loop.
Is there a simple way to create an iterable object that will behave like a correct range based for-loop on float
s?
回答1:
Here is my attempt which does not impair the semantics of iterators. Now, each iterator knows its stopping value. The iterator will set itself to this value upon exceeding it. All end iterators of a range with equal to
therefore compare equal.
template <typename T>
struct range {
T from, to;
range(T from, T to): from(from), to(to) {}
struct iterator {
const T to; // iterator knows its bounds
T current;
T operator*() { return current; }
iterator& operator++() {
++current;
if(current > to)
// make it an end iterator
// (current being exactly equal to 'current' of other end iterators)
current = to;
return *this;
}
bool operator==(const iterator& other) const // OT: note the const
{ return current == other.current; }
// OT: this is how we do !=
bool operator!=(const iterator& other) const { return !(*this == other); }
};
iterator begin() const { return iterator{to, from}; }
iterator end() const { return iterator{to, to}; }
};
Why is this better?
The solution by @JeJo relies on the order in which you compare those iterators, i.e. it != end
or end != it
. But, in the case of range-based for, it is defined. Should you use this contraption in some other context, I advise the above approach.
Alternatively, if sizeof(T) > sizeof(void*)
, it makes sense to store a pointer to the originating range
instance (which in the case of the range-for persists until the end) and use that to refer to a single T
value:
template <typename T>
struct range {
T from, to;
range(T from, T to): from(from), to(to) {}
struct iterator {
range const* range;
T current;
iterator& operator++() {
++current;
if(current > range->to)
current = range->to;
return *this;
}
...
};
iterator begin() const { return iterator{this, from}; }
iterator end() const { return iterator{this, to}; }
};
Or it could be T const* const
pointing directly to that value, it is up to you.
OT: Do not forget to make the internals private
for both classes.
回答2:
Instead of a range object you could use a generator (a coroutine using co_yield
). Despite it is not in the standard (but planned for C++20), some compilers already implement it.
See: https://en.cppreference.com/w/cpp/language/coroutines
With MSVC it would be:
#include <iostream>
#include <experimental/generator>
std::experimental::generator<double> rangeGenerator(double from, double to) {
for (double x=from;x <= to;x++)
{
co_yield x;
}
}
int main()
{
for (auto i : rangeGenerator(5.1, 9.2))
std::cout << i << ' '; // prints 5.1 6.1 7.1 8.1 9.1
}
回答3:
Is there a simple way to create an iterable object that will behave like a correct for loop on
float
s?
The simplest hack† would be using the traits std::is_floating_point to provide different return (i.e. iter <= end
) within the operator!=
overload.
(See Live)
#include <type_traits>
bool operator!=(const iterator& other)
{
if constexpr (std::is_floating_point_v<T>) return current <= other.current;
return !(*this == other);
}
†Warning: Even though that does the job, it breaks the meaning of operator!=
overload.
Alternative Solution
The entire range
class can be replaced by a simple function in which the values of the range will be populated with the help of std::iota
in the standard container std::vector.
Use SFINE, to restrict the use of the function for only the valid types. This way, you can rely on standard implementations and forget about the reinventions.
(See Live)
#include <iostream>
#include <type_traits>
#include <vector> // std::vector
#include <numeric> // std::iota
#include <cstddef> // std::size_t
#include <cmath> // std::modf
// traits for valid template types(integers and floating points)
template<typename Type>
using is_integers_and_floats = std::conjunction<
std::is_arithmetic<Type>,
std::negation<std::is_same<Type, bool>>,
std::negation<std::is_same<Type, char>>,
std::negation<std::is_same<Type, char16_t>>,
std::negation<std::is_same<Type, char32_t>>,
std::negation<std::is_same<Type, wchar_t>>
/*, std::negation<std::is_same<char8_t, Type>> */ // since C++20
>;
template <typename T>
auto ragesof(const T begin, const T end)
-> std::enable_if_t<is_integers_and_floats<T>::value, std::vector<T>>
{
if (begin >= end) return std::vector<T>{}; // edge case to be considered
// find the number of elements between the range
const std::size_t size = [begin, end]() -> std::size_t
{
const std::size_t diffWhole
= static_cast<std::size_t>(end) - static_cast<std::size_t>(begin);
if constexpr (std::is_floating_point_v<T>) {
double whole; // get the decimal parts of begin and end
const double decimalBegin = std::modf(static_cast<double>(begin), &whole);
const double decimalEnd = std::modf(static_cast<double>(end), &whole);
return decimalBegin <= decimalEnd ? diffWhole + 1 : diffWhole;
}
return diffWhole;
}();
// construct and initialize the `std::vector` with size
std::vector<T> vec(size);
// populates the range from [first, end)
std::iota(std::begin(vec), std::end(vec), begin);
return vec;
}
int main()
{
for (auto i : ragesof( 5, 9 ))
std::cout << i << ' '; // prints 5 6 7 8
std::cout << '\n';
for (auto i : ragesof(5.1, 9.2))
std::cout << i << ' '; // prints 5.1 6.1 7.1 8.1 9.1
}
回答4:
A floating-point loop or iterator should typically use integer types to hold the total number of iterations and the number of the current iteration, and then compute the "loop index" value used within the loop based upon those and loop-invariant floating-point values.
For example:
for (int i=-10; i<=10; i++)
{
double x = i/10.0; // Substituting i*0.1 would be faster but less accurate
}
or
for (int i=0; i<=16; i++)
{
double x = ((startValue*(16-i))+(endValue*i))*(1/16);
}
Note that there is no possibility of rounding errors affecting the number of iterations. The latter calculation is guaranteed to yield a correctly-rounded result at the endpoints; computing startValue+i*(endValue-startValue)
would likely be faster (since the loop-invariant (endValue-startValue)
can be hoisted) but may be less accurate.
Using an integer iterator along with a function to convert an integer to a floating-point value is probably the most robust way to iterate over a range of floating-point values. Trying to iterate over floating-point values directly is far more likely to yield "off-by-one" errors.
来源:https://stackoverflow.com/questions/56217541/how-to-create-a-range-like-iterable-object-of-floats