如何创建一个类似"范围"的浮点数可迭代对象?

How to create a `range`-like iterable object of floats?

本文关键字:quot 范围 浮点数 对象 迭代 创建 何创建 一个      更新时间:2023-10-16

我想在 c++ 中创建一个类似range的结构,它将像这样使用:

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

处理整数大小写相对容易:

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 }; }
};

但是,这在float情况下不起作用,因为C++中的标准基于范围的循环会检查是否iter==end,而不是检查是否iter <= end,就像您在 for 循环中所做的那样。

有没有一种简单的方法来创建一个可迭代的对象,该对象的行为类似于基于floats 的正确范围的 for 循环

这是我的尝试,它不会损害迭代器的语义。现在,每个迭代器都知道其停止值。迭代器将在超过此值时将自身设置该值。因此,具有相等to范围的所有结束迭代器进行比较相等。

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}; }
};

为什么这样更好?

@JeJo的解决方案取决于你比较这些迭代器的顺序,即it != endend != it.但是,在基于范围的 for 的情况下,它是定义的。如果您在其他情况下使用此装置,我建议使用上述方法。


或者,如果sizeof(T) > sizeof(void*),则存储指向原始range实例的指针(在 range-for 的情况下持续到最后)并使用它来引用单个T值是有意义的:

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}; }
};

或者T const* const直接指向该值,这取决于您。

OT:不要忘记使两个类的内部结构private

您可以使用生成器(使用co_yield的协程)代替范围对象。尽管它不在标准中(但计划在 C++20 年),但一些编译器已经实现了它。

请参阅:https://en.cppreference.com/w/cpp/language/coroutines

使用 MSVC,它将是:

#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
}

有没有一种简单的方法来创建一个可迭代的对象,该对象的行为 就像floatS 上的正确 for 循环一样?

最简单的技巧是使用std::is_floating_point的特征来提供不同的回报(即iter <= end)在operator!=内过载。

(观看直播)

#include <type_traits>
bool operator!=(const iterator& other)
{
if constexpr (std::is_floating_point_v<T>) return current <= other.current;
return !(*this == other);
}

警告:即使这样做,它也破坏了operator!=过载的意义


替代解决方案

整个range类可以用一个简单的函数代替,在该函数中,范围的值将在标准容器std::vector中的std::iota的帮助下填充。

使用SFINE限制仅对有效类型使用函数。 这样,您可以依靠标准实现而忘记重新发明。

(观看直播)

#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
}

浮点循环或迭代器通常应使用整数类型来保存迭代总数和当前迭代次数,然后根据这些值和循环不变浮点值计算循环中使用的"循环索引"值。

例如:

for (int i=-10; i<=10; i++)
{
double x = i/10.0;  // Substituting i*0.1 would be faster but less accurate
}

for (int i=0; i<=16; i++)
{
double x = ((startValue*(16-i))+(endValue*i))*(1/16);
}

请注意,舍入误差不可能影响迭代次数。 后一种计算保证在端点处产生正确舍入的结果;计算startValue+i*(endValue-startValue)可能会更快(因为可以提升循环不变(endValue-startValue)),但可能不太准确。

使用整数迭代器以及函数将整数转换为浮点值可能是迭代一系列浮点值的最可靠方法。 尝试直接迭代浮点值更有可能产生"逐一"错误。