【问题标题】:How to create a `range`-like iterable object of floats?如何创建一个类似于“范围”的可迭代浮点对象?
【发布时间】:2019-10-06 14:57:47
【问题描述】:

我想在 中创建一个类似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 &lt;= end,就像您在for 循环中所做的那样。

有没有一种简单的方法来创建一个可迭代对象,它的行为类似于floats 上的正确基于范围的for-loop

【问题讨论】:

  • 可能是 operator== 的特化,用于浮点类型,通过使用 current&lt;=other.current 来颠覆语义?
  • 如何实现一个特殊的end 迭代器,当增量值超过to 时将在operator++() 中设置?
  • 既然提到了协程,为什么不使用即将到来的ranges library呢? (或者 the range library 是标准的基础?)
  • 您应该知道浮点舍入会影响您的循环。例如,对于 double 常用的 IEEE-754 格式,for (double x = 1.03; x &lt;= 11.03; x += 1) 将在 x 约为 10.03 而不是 11.03 时结束。它将递增到11.03000000 000501,01198828125,但11.03 987654341 @ 98765432715988159999999881599999998815999999885999999988599999998859999998759999875,4876599999998859999999885913781590999999998875,so,so @ 987654396815909832715988599999998815999999988599999875,所以x &lt;= 11.03评估为false。 span>
  • 使用linspace 风格的显式元素计数(并且没有默认计数,与 MATLAB 或 numpy linspace 不同)比从步长值开始并推导元素数量要安全得多从那里。面向计数而不是面向步长的方法消除了意外包含或排除端点的问题。

标签: c++ c++ templates floating-point iterator c++17


【解决方案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 != endend != it。但是,在基于范围的情况下,it is defined。如果您在其他情况下使用此装置,我建议您采用上述方法。


或者,如果sizeof(T) &gt; 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 的协程)来代替范围对象。尽管它不在标准中(但计划用于 C++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】:

      有没有一种简单的方法来创建一个可迭代的对象 喜欢floats 上的正确 for 循环?

      最简单的 hack 将使用特征 std::is_floating_point 在 @987654330 中提供不同的回报(即 iter &lt;= end) @重载。

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

      警告:即使这样做了,它也破坏了 operator!= 过载的含义


      替代解决方案

      整个range 类可以替换为一个简单的函数,在该函数中,将在std::iota 的帮助下填充范围的值 在标准容器中std::vector

      使用SFINE,限制仅对有效类型使用该函数。 这样一来,您就可以依赖标准实现而忘记重新发明。

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

      【讨论】:

        【解决方案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) 可以提升)但可能不太准确。

        使用整数迭代器和函数将整数转换为浮点值可能是迭代一系列浮点值的最可靠方法。尝试直接迭代浮点值更有可能产生“非一”错误。

        【讨论】:

          猜你喜欢
          • 2012-07-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-02-07
          • 1970-01-01
          • 2012-08-24
          • 1970-01-01
          相关资源
          最近更新 更多