如何创建一个类似于“范围”的浮动对象?

Ere*_*evi 24 c++ floating-point templates iterator c++17

我想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
Run Code Online (Sandbox Code Playgroud)

处理整数情况相对容易:

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 }; }
};
Run Code Online (Sandbox Code Playgroud)

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

有一个简单的方法来创建一个迭代的对象,将像一个正确的基于for循环范围floatS'

Log*_*uff 17

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}; }
};
Run Code Online (Sandbox Code Playgroud)

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}; }
};
Run Code Online (Sandbox Code Playgroud)

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.


P. *_*ARD 14

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
}
Run Code Online (Sandbox Code Playgroud)


JeJ*_*eJo 9

Is there a simple way to create an iterable object that will behave like a correct for loop on floats?

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);
}
Run Code Online (Sandbox Code Playgroud)

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
}
Run Code Online (Sandbox Code Playgroud)


sup*_*cat 5

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

例如:

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

要么

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

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

使用整数迭代器和将整数转换为浮点值的函数可能是在一系列浮点值上进行迭代的最可靠的方法。尝试直接迭代浮点值的可能性更大,可能会产生“一对一”错误。