c++ - 如何创建浮点数的“范围”类可迭代对象?




floating-point iterator (3)

我想在 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 就像你在循环中那样。

有没有一种简单的方法来创建一个可迭代的对象,它在 float s上表现得像一个正确的for循环?


有没有一种 简单的方法 来创建一个可迭代的对象,它在 float s上表现得像一个正确的for循环?

最简单的hack 将使用特征 std::is_floating_point operator!= overload中提供不同的返回(即 iter <= end )。

见直播

#include <type_traits>

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

警告:即使这样做,它也会破坏 operator!= 的意思 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
}

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

请参阅: https://en.cppreference.com/w/cpp/language/coroutineshttps://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
}

这是我的尝试,它不会妨碍迭代器的语义。 现在,每个迭代器都知道它的停止值,它会在超过它时自行设置。 所有范围的结束迭代器都等于因此比较相等。

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 。 但是,在基于范围的情况下, 它被定义 。 如果你在其他方面使用这个装置,我建议采用上述方法。

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

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

或者它可能是 T const* const 直接指向该值,它取决于您。

OT:不要忘记让两个班级的内部都是 private 的。





range