Как создать итеративный объект типа float типа range? - PullRequest
25 голосов
/ 20 мая 2019

Я хочу создать 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, как это было бы в a дляloop.

Существует ли простой способ создания итерируемого объекта, который будет вести себя как правильный цикл for в float s?

Ответы [ 4 ]

17 голосов
/ 20 мая 2019

Вот моя попытка, которая не нарушает семантику итераторов.Изменение состоит в том, что теперь каждый итератор знает свое значение остановки, которому он будет устанавливать себя при превышении.Поэтому все конечные итераторы диапазона с равным 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 != end или end != 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 {
        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, указывающий непосредственно на это значение, оно доВы.

ОТ: Не забудьте сделать внутреннее private для обоих классов.

13 голосов
/ 20 мая 2019

Вместо объекта диапазона вы можете использовать генератор (сопрограмму, использующую 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
}
8 голосов
/ 20 мая 2019

Есть ли простой способ для создания итерируемого объекта, который будет вести себя как правильный цикл для float s?

Самый простой хак будет использовать черты std::is_floating_point, чтобы обеспечить различный возврат (т. Е. iter <= end) в пределах operator!= перегрузка.

( см. В прямом эфире )

#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 , чтобы ограничить использование функции только для допустимых типов. Таким образом, вы можете положиться на стандартные реализации и забыть о переизобретениях.

( см. В прямом эфире )

#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
}
5 голосов
/ 20 мая 2019

Цикл или итератор с плавающей запятой обычно должен использовать целочисленные типы для хранения общего числа итераций и номера текущей итерации, а затем вычислять значение «индекса цикла», используемого в цикле, на основе этих значений и инварианта циклаЗначения с плавающей точкой.

Например:

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) может быть поднят), но может быть менее точным.

Использование целочисленного итератора вместе с функцией для преобразования целого числа в число с плавающей запятойЗначение, вероятно, самый надежный способ перебора диапазона значений с плавающей запятой.Попытка итерации по значениям с плавающей запятой напрямую с гораздо большей вероятностью приведет к ошибкам "off-by-one".

...