c++ floating-point iterator range

c++ - ¿Cómo crear un objeto iterable de flotadores?



floating-point iterator (4)

¿Existe una forma sencilla de crear un objeto iterable que se comporte como un bucle for correcto en float ?

El truco más simple estaría utilizando los rasgos std::is_floating_point para proporcionar un retorno diferente (es decir, iter <= end ) dentro del operator!= Overload.

( Ver en vivo )

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

Advertencia: aunque eso haga el trabajo, rompe el significado de operator!= Sobrecarga .

Solución alternativa

La clase de range completo puede reemplazarse por una función simple en la que los valores del rango se rellenarán con la ayuda de std::iota en el contenedor estándar std::vector .

Use SFINE , para restringir el uso de la función solo para los tipos válidos. De esta manera, puede confiar en implementaciones estándar y olvidarse de las reinvenciones.

( Ver en vivo )

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

Quiero crear una construcción similar a un range en c ++ , que se usará así:

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

Manejar el caso entero es relativamente fácil:

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

Sin embargo, esto no funciona en el caso float , ya que el bucle basado en rango estándar en C++ verifica si iter==end , y no si iter <= end como lo haría en un bucle for a.

¿Existe una forma sencilla de crear un objeto iterable que se comporte como un bucle for correcto en float ?


Aquí está mi intento, que no obstaculiza la semántica de los iteradores. El cambio es que ahora, cada iterador conoce su valor de detención, al cual se establecerá al sobrepasarlo. Todos los iteradores finales de un rango con igual, to tanto, se comparan igual.

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

¿Por qué es esto mejor?

La solución de @JeJo se basa en el orden en el que se comparan esos iteradores, es decir, it != end o end != it It. Pero, en el caso de rango basado en, se define . Si usa este artilugio en algún otro contexto, le aconsejo el enfoque anterior.

Alternativamente, si sizeof(T) > sizeof(void*) , tiene sentido almacenar un puntero a la instancia de range origen (que en el caso de range-for persiste hasta el final) y usarlo para referirse a una sola T valor:

template <typename T> struct range { T from, to; range(T from, T to): from(from), to(to) {} struct iterator { const range* 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}; } };

O podría ser T const* const apunta directamente a ese valor, depende de usted.

OT: No te olvides de hacer los internos private para ambas clases.


En lugar de un objeto de rango, podría usar un generador (una corrutina que usa co_yield ). A pesar de que no está en el estándar (pero planeado para C ++ 20), algunos compiladores ya lo implementan.

Consulte: https://en.cppreference.com/w/cpp/language/coroutines

Con MSVC sería:

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


Un bucle o iterador de punto flotante normalmente debe usar tipos enteros para contener el número total de iteraciones y el número de la iteración actual, y luego calcular el valor del "índice de bucle" usado dentro del bucle basado en esos y en un punto flotante invariante del bucle valores.

Por ejemplo:

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

o

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

Tenga en cuenta que no hay posibilidad de redondear los errores que afectan el número de iteraciones. Se garantiza que este último cálculo arrojará un resultado correctamente redondeado en los puntos finales; computar startValue+i*(endValue-startValue) probablemente sea más rápido (ya que el bucle invariante (endValue-startValue) puede ser izado) pero puede ser menos preciso.

El uso de un iterador entero junto con una función para convertir un entero en un valor de punto flotante es probablemente la forma más robusta de iterar en un rango de valores de punto flotante. Tratar de iterar sobre valores de punto flotante directamente es mucho más probable que produzca errores "off-by-one".