3

I would like to write a struct which will generate an infinite sequence of fibonacci numbers in a way compatible with std::ranges and range adaptors.

So if I wanted the first 5 even fibonacci numbers, I would write something like this:

#include <iostream>
#include <ranges>

using namespace std;

int main() {
  auto is_even = [](auto a) { return a % 2 == 0; };
  for (auto v :
       something | ranges::views::filter(is_even) | ranges::views::take(5))
    std::cout << v << std::endl;

  return 0;
}

What "something" needs to be ?

It seems to me that it has to be a class with a forward iterator, but I can't find any example.

Dorian
  • 490
  • 2
  • 10
  • 2
    integer overflow makes the infinite part impossible unless you also bring in a big number library. I believe a `std::uint64_t` will only go through ~90 iterations before wrapping. – sweenish Jan 19 '22 at 20:48

1 Answers1

5

Edit: As it was pointed out by 康桓瑋 in the comments, there has been a better, cleaner solution presented at Cppcon, link, by Tristan Brindle.

I think this could serve as a quick reference to make custom iterator-based generators.

By your requirements, something must be a std::ranges::view, meaning it must be a moveable std::ranges::range deriving from std::ranges::view_interface<something>.

We can tackle all three with the following:

#include <ranges>

template<typename T>
class fib : public std::ranges::view_interface<fib<T>>{
public:
    struct iterator;


    auto begin() const { return iterator{}; }
    auto end() const { return std::unreachable_sentinel; }

};

Notice std::unreachable_sentinel which makes creating sequences without end really simple.

We still have to define the iterator which does the actual work. In your case we want fib to be "the source" of the values, so our iterator should actually be std::input_iterator. There's some boiler plate code needed for that but it's basically just a type which can be incremented and dereferenced to yield its current value.

Something like this will do:

#include <iterator>

template<typename T>
struct fib<T>::iterator {
    using iterator_category = std::input_iterator_tag;
    using value_type = T;
    using difference_type = std::ptrdiff_t;
    using pointer = T*;
    using reference = T;

    constexpr iterator() noexcept = default;

    iterator& operator++() {
        auto old_next = next;
        next = next + current;
        current = old_next;

        return *this;
    }

    iterator operator++(int) {
        iterator current{*this};
        ++(*this);
        return current;
    }

    value_type operator*() const {
        return current;
    }

    bool operator==(const iterator& other) const { return current == other.current && next==other.next; }

private:
    T current= {};
    T next = T{} + 1; // Could perhaps be fancier.
};

The increment operator does the computation itself, it's the simple iterating algorithm.

That's it, here's a working example:

#include <cstdint>
#include <iostream>
int main() {
  auto is_even = [](auto a) { return a % 2 == 0; };
  for (auto v :
       fib<std::uint64_t>{} | std::ranges::views::filter(is_even) | std::ranges::views::take(10))
    std::cout << v << std::endl;

  return 0;
}

which outputs:

0
2
8
34
144
610
2584
10946
46368
196418

Of course you won't get very far even with std::uint64_t. But T can be anything numeric enough.

One can easily generalize the iterator to hold a stateful functor, likely passed from the range itself, call it during each increment and store the yielded value for dereferencing later. This would be very crude, but simple, way how to at least simulate "yield-based" generators.

Quimby
  • 17,735
  • 4
  • 35
  • 55
  • 1
    Your iterator can be `forward_iterator_tag`, you don't need `pointer` or `reference` (especially since `reference` is incorrect: your iterator's `reference` is `T`, since that's what `operator*` returns). You can also just `return std::unreachable_sentinel;` – Barry Jan 19 '22 at 21:20
  • 1
    Some pointers to the requirements for ranges from the standard would be helpful. For example, that a [std::ranges::range](https://en.cppreference.com/w/cpp/ranges/range) requires `begin()` and `end()`, that those members should return [forward iterators](https://en.cppreference.com/w/cpp/iterator/forward_iterator), and so on. – G. Sliepen Jan 19 '22 at 21:21
  • @Barry Okay, I admit I am not an expert on custom iterators, this was the simplest category I though would be appropriate. Point taken on the rest. Thank you. – Quimby Jan 19 '22 at 21:25
  • @G.Sliepen Thank you for adding the links, I was about to do that, you were quicker :) – Quimby Jan 19 '22 at 21:29
  • 1
    No problem, I was actually writing an answer myself and you beat me to it :) – G. Sliepen Jan 19 '22 at 21:30
  • Is `T next = current + 1` fancier? – G. Sliepen Jan 19 '22 at 21:31
  • 1
    @G.Sliepen Sure, but I personally do not like chained initialization of members cause it causes them to depend on the declaration order. I know that compilers nowadays emit warnings if that happens but still, feels like too hidden complexity for me. – Quimby Jan 19 '22 at 21:36
  • 3
    Tristan Brindle presented the full implementation of `fibonacci_view` in his [recent cppcon](https://www.youtube.com/watch?v=3MBtLeyJKg0) talk, just take a look. – 康桓瑋 Jan 20 '22 at 01:44
  • @康桓瑋 Cool, that looks even better than my solution, especially the `rng::sub_range` helper. – Quimby Jan 20 '22 at 07:39
  • Isn't the operator== incorrect/confusing by only comparing the current-value and not next; as the fibonacci sequence is 0,1,1,2,... and the 2nd and 3rd element will compare equal? (I would also rename 'current' in operator++(int) to reduce confusion.) – Hans Olsson Jan 20 '22 at 08:29
  • Thanks for your responses. Obviously the question used the concrete fibonacci example (an infinite sequence with memory) to get an answer adaptable to a more generic case about sequence generation and ranges. Should I change the title of the question to something more generic or is it better to leave it like that ? – Dorian Jan 20 '22 at 08:52
  • @Dorian I think you can leave it :) – Quimby Jan 20 '22 at 09:29
  • @HansOlsson Sorry, missed your comment. Good catch, you are indeed correct, I will fix that. – Quimby Jan 20 '22 at 23:27