Search code examples
c++vectoriteratorc++20

Compiler is requiring a custom iterator to have an integral type for it to be a considered forward iterator


I'm writing an iterator for a custom collection:

template <typename T>
class List
{
public:
    class Iterator
    {

    public:

        using difference_type = T;
        using value_type = T;
        using pointer = const T*;
        using reference = const T&;
        using iterator_category = std::forward_iterator_tag;

        Iterator(T* ptr = nullptr) : _ptr(ptr) {}
        Iterator& operator++() { _ptr++; return *this; }
        Iterator operator++(int) { Iterator retval = *this; ++(*this); return retval; }
        std::strong_ordering operator<=>(Test& other) { return _x <=> other._x; }
        bool operator==(Iterator other) const { return _ptr == other._ptr; }
        bool operator!=(Iterator other) const { return !(_ptr == other._ptr); }
        reference operator*() const { return *_ptr; }
        pointer operator->() const { return _ptr; }
        reference operator=(const T& value) { *_ptr = value; return value; }

    private:
        T* _ptr;

    };

    Iterator begin() { return Iterator(_data); }
    Iterator end() { return Iterator(_data + 4); }

    List() // Generate generic data
    {
        for (int i = 0; i < 5; i++)
        {
            _data[i] = i + 1;
        }
    }

private:
    T _data[5];
};

This needs to be a forward iterator, but when I check this with a static assert, I can only use List<T> instances where the type T is an integral type. Below is an example of the non-integral types float and a small Test class. I also tested the default std::vector<Test> iterator with an identical static assert and didn't get an error.

class Test
{
public:
    Test(int x = 0) : _x(x) {}
    void Increment() { _x++; }
    std::strong_ordering operator<=>(Test& other) { return _x <=> other._x; }
    int operator=(int x) { _x = x; return x; }
    friend std::ostream& operator<<(std::ostream& os, const Test& m) { return os << m._x; };

private:
    int _x;
};

int main()
{
    List<int> container1;
    List<Test> container2;
    List<float> container3;
    std::vector<Test> container4;

    static_assert(std::forward_iterator<decltype(container1.begin())>); // no error
    static_assert(std::forward_iterator<decltype(container2.begin())>); // ERROR
    static_assert(std::forward_iterator<decltype(container3.begin())>); // ERROR
    static_assert(std::forward_iterator<decltype(container4.begin())>); // no error

    // This loop works correctly if the static asserts are commented out
    for (List<int>::Iterator it = container1.begin(); it != container1.end(); it++)
    {
        std::cout << *it << "\n";
    }


    return 0;
}

I'm using the C++20 ISO standard with the Visual C++ compiler. Expanding the errors gets me this error stack trace ending with the error "the constraint was not satisfied". The error references this line in __msvc_iter_core.hpp:

template <class _Ty>
concept _Integer_like = _Is_nonbool_integral<remove_cv_t<_Ty>> || _Integer_class<_Ty>;

I don't know why an integral type would be required. I don't think the Test class is the problem since the assert works with a vector iterator, but I don't see anything in the Iterator class that would require T to be integral. Why is this requirement being tested?


Solution

  • The issue here is

    using difference_type = T;
    

    ... which is almost certainly wrong. Remember that the difference_type represents the difference between two iterators, and this usually doesn't depend on what they're iterating over.

    Note that every iterator concept (such as std::forward_iterator<I> at the very least requires std::weakly_incrementable<I>, which has the nested requirement

    typename iter_difference_t<I>;
    requires is-signed-integer-like<iter_difference_t<I>>;
    

    Having any difference_type alias at all would satisfy the first part, however, you need a signed-integer-like type as the difference the satisfy the second part. This doesn't have to be an integral type; it can also be something very similar (e.g. MSVC's std::_Signed128 type for std::ranges::iota_view). This is also why the standard library is testing for std::_Integer_like, not something like std::integral.

    The most easy way to fix your problem would be

    using difference_type = std::ptrdiff_t;
    

    Unless you need extremely large differences which couldn't be represented by std::ptrdiff_t, you should use that type as a difference.