Как создать итеративный объект -like с плавающей точкой?
Я хочу создать range
-like конструкцию в c++, которая будет использоваться следующим образом:
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 на float
?
Ответы
Ответ 1
Вот моя попытка, которая не нарушает семантику итераторов. Изменение состоит в том, что теперь каждый итератор знает свое значение остановки, которому он будет устанавливать себя при превышении. Все конечные итераторы диапазона с равным 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 != end
или end != it
It. Но, в случае с диапазоном, он определен. Если вы используете эту штуковину в каком-то другом контексте, я советую использовать вышеуказанный подход.
Альтернативно, если 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
для обоих классов.
Ответ 2
Вместо объекта диапазона вы можете использовать генератор (сопрограмму с использованием co_yield
). Несмотря на то, что это не входит в стандарт (но планируется для С++ 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
}
Ответ 3
Есть ли простой способ создать итерируемый объект, который будет вести себя как правильный цикл for на float
s?
Простейшим хаком † было бы использовать черты std::is_floating_point
для обеспечения различного возврата (т. std::is_floating_point
iter <= end
) внутри operator!=
Overload.
(См. В прямом эфире)
#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::iota
в стандартном контейнере std::vector
.
Используйте 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
}
Ответ 4
Цикл или итератор с плавающей точкой обычно должен использовать целочисленные типы для хранения общего числа итераций и номера текущей итерации, а затем вычислять значение "индекса цикла", используемого в цикле, на основе этих переменных и инвариантной для цикла плавающей запятой ценности.
Например:
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)
), но может быть менее точным.
Использование целочисленного итератора вместе с функцией для преобразования целого числа в значение с плавающей запятой, вероятно, является наиболее надежным способом перебора диапазона значений с плавающей запятой. Попытка итерации по значениям с плавающей запятой напрямую с большей вероятностью приведет к ошибкам типа "off-by-one".