Debugging Custom Implementations of Ranges with Chat-GPT

I’ve been looking into building std::ranges::enumerate by hand. I’ve been using Chat GPT for assistance with this, and the results are impressive, but also a great learning experience. Let me illustrate.

I started with the query:

How would I write ranges::enumerate in C++20?

Here’s basically the answer I got from Chat-GPT:

#include <iostream>
#include <vector>
#include <ranges>

template <std::ranges::input_range R>
auto enumerate(R&& range) {
    using std::begin, std::end;
    using iterator_t = decltype(begin(range));
    using index_t = std::make_signed_t<std::ranges::range_difference_t<R>>;

    struct iterator {
        iterator_t iter;
        index_t index;

        decltype(auto) operator*() const {
            return std::pair{index, *iter};
        }

        iterator& operator++() {
            ++iter;
            ++index;
            return *this;
        }

        bool operator!=(const iterator& other) const {
            return iter != other.iter;
        }
    };

    struct sentinel {
        iterator_t end;

        bool operator!=(const iterator& iter) const {
            return iter.iter != end;
        }
    };

    return std::ranges::subrange(iterator{begin(range), 0}, sentinel{end(range)});
}

int main() {
    std::vector<std::string> words = {"apple", "banana", "cherry"};

    for (auto [index, word] : enumerate(words)) {
        std::cout << "Index: " << index << ", Value: " << word << std::endl;
    }

    return 0;
}

That’s a pretty impressive answer, but it’s not right. Looking at the definition of subrange:

template<
    std::input_or_output_iterator I,
    std::sentinel_for<I> S = I,
    ranges::subrange_kind K = std::sized_sentinel_for<S, I> ?
        ranges::subrange_kind::sized : ranges::subrange_kind::unsized
>
    requires (K == ranges::subrange_kind::sized || !std::sized_sentinel_for<S, I>)
class subrange : public ranges::view_interface<subrange<I, S, K>>

We see one of the requirements of I (or iterator in our case), is to be a std::sentinel_for<I>. Looking at the definition of the concept std::sentinel_for, we see:

template< class S, class I >
    concept sentinel_for =
        std::semiregular<S> &&
        std::input_or_output_iterator<I> &&
        __WeaklyEqualityComparableWith<S, I>;

So that shows that we need to make sure that iterator and sentinel are weakly comparible, which they aren’t – no comparison overloads have been added to those classes.

What’s impressive is that when I point that out to Chat-GPT, it corrects it, giving this new answer:

#include <iostream>
#include <vector>
#include <ranges>

template <std::ranges::input_range R>
auto enumerate(R&& range) {
    using std::begin, std::end;
    using iterator_t = decltype(begin(range));
    using index_t = std::make_signed_t<std::ranges::range_difference_t<R>>;

    struct iterator {
        iterator_t iter;
        index_t index;

        decltype(auto) operator*() const {
            return std::pair{index, *iter};
        }

        iterator& operator++() {
            ++iter;
            ++index;
            return *this;
        }

        bool operator!=(const iterator& other) const {
            return iter != other.iter;
        }
    };

    return std::ranges::subrange(iterator{begin(range), 0}, iterator{end(range), 0});
}

int main() {
    std::vector<std::string> words = {"apple", "banana", "cherry"};

    for (auto [index, word] : enumerate(words)) {
        std::cout << "Index: " << index << ", Value: " << word << std::endl;
    }

    return 0;
}

Wow. But putting this into godbolt fails to compile. And this is where we can use a couple of debugging techniques to show why.

The code fails to compile with: error: no viable constructor or deduction guide for deduction of template arguments of 'subrange'. That’s probably because it cannot deduce the types of subrange, likely because the arguments supplied do not conform to the requirements. We can check that easily. Recall the definition of subrange was:

template<
    std::input_or_output_iterator I,
    std::sentinel_for<I> S = I,
    ranges::subrange_kind K = std::sized_sentinel_for<S, I> ?
        ranges::subrange_kind::sized : ranges::subrange_kind::unsized
>
    requires (K == ranges::subrange_kind::sized || !std::sized_sentinel_for<S, I>)
class subrange : public ranges::view_interface<subrange<I, S, K>>

So let’s add a quick static_assert check in the code to see if our iterator class conforms to those requirements. I add:

    static_assert(std::input_or_output_iterator<iterator>);

And that fails. Interesting. Well, looking at the definition for std::input_or_output_iterator, we not that we need to have a difference_type and both post-fix and pre-fix operators. And those are missing in the Chat-GPT version. So if we add those:

    struct iterator {
        iterator_t iter;
        index_t index;
        using difference_type = index_t;

        decltype(auto) operator*() const {
            return std::pair{index, *iter};
        }

        iterator& operator++() {
            ++iter;
            ++index;
            return *this;
        }

        iterator operator++(int) {
            auto old = *this;
            ++iter;
            ++index;
            return old;
        }

        bool operator!=(const iterator& other) const {
            return iter != other.iter;
        }
    };

well, we still fail. But look at the definition for std::sentinel_for:

template< class S, class I >
    concept sentinel_for =
        std::semiregular<S> &&
        std::input_or_output_iterator<I> &&
        __WeaklyEqualityComparableWith<S, I>;

It needs to be Comparable. And while iterator has a != comparision operator, it doesn’t have the == comparision. So adding that comparision and we get the full working solution:

#include <iostream>
#include <vector>
#include <ranges>

template <std::ranges::input_range R>
auto enumerate(R&& range) {
    using std::begin, std::end;
    using iterator_t = decltype(begin(range));
    using index_t = std::make_signed_t<std::ranges::range_difference_t<R>>;

    struct iterator {
        iterator_t iter;
        index_t index;
        using difference_type = index_t;

        decltype(auto) operator*() const {
            return std::pair{index, *iter};
        }

        iterator& operator++() {
            ++iter;
            ++index;
            return *this;
        }

        iterator operator++(int) {
            auto old = *this;
            ++iter;
            ++index;
            return old;
        }

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

        bool operator!=(const iterator& other) const {
            return iter != other.iter;
        }
    };

    static_assert(std::input_or_output_iterator<iterator>);
    return std::ranges::subrange(iterator{begin(range), 0}, iterator{end(range), 0});
}

int main() {
    std::vector<std::string> words = {"apple", "banana", "cherry"};

    for (auto [index, word] : enumerate(words)) {
        std::cout << "Index: " << index << ", Value: " << word << std::endl;
    }

    return 0;
}
Written on December 10, 2023