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".