Search code examples
c++iteratorc++20c++-concepts

Create contiguous_iterator for custom class


Summary

I have a custom array class:

template<typename T, int SIZE>
class Array {
private:
    T mArray[SIZE];
};

To enable support for std algorithms, with ranges, I want to create an iterator for this class. It would seem that std::contiguous_iterator would be the optimal choice since I can guarantee contiguous memory layout for the data. Following the iterator tutorial I should create a class inside this class. However, I should somehow be (quoted) "For example, instead of the std::forward_iterator_tag tag you would mark your iterator with the std::forward_iterator concept.".

I have a hard time figuring out what the syntax would look like for this, and I have been unable to find a post on the web showcasing this.

Question

How do I complete the following code snippet to implement std::contiguous_iterator for my Array<T,S> class?:

import <iterator>;

template<typename T, int SIZE>
class Array {
public:
    const T& operator[](int i) { return mArray[i]; }
    T& operator[](int i) { return mArray[i]; }

private:
    T mArray[SIZE];

public:
    struct Iterator {
        Iterator(T* ptr) : mPtr(ptr) {}

    private:
        T* mPtr;
    };

    Iterator begin() { return Iterator(&mArray[0]); }
    Iterator end() { return Iterator(&mArray[SIZE]); }
};

NOTE: There is a lot of operator overloads. An answer is not required to provide all of them. I just need an example syntax for where to place the concept, then I can probably figure out the rest.


Solution

  • Credits

    Thanks to @glenn-teitelbaum for pointing me in the right direction. I think I managed to figure out how to do this. It took a long time, so this will hopefully save someone else that trouble.

    [Answering my own question]

    The iterator should comply with the std::contiguous_iterator concept, so I looked at cppreference for the necessary parts. The concept is defined like this:

    template<class I>
      concept contiguous_iterator =
        std::random_access_iterator<I> &&
        std::derived_from</*ITER_CONCEPT*/<I>, std::contiguous_iterator_tag> &&
        std::is_lvalue_reference_v<std::iter_reference_t<I>> &&
        std::same_as<
          std::iter_value_t<I>, std::remove_cvref_t<std::iter_reference_t<I>>
        > &&
        requires(const I& i) {
          { std::to_address(i) } ->
            std::same_as<std::add_pointer_t<std::iter_reference_t<I>>>;
        };
    

    So in order to implement this concept, I must first also implement std::random_access_iterator, std::is_derived_from<[...]>, std::is_lvalue_reference_v<[...]>, and so on. This is a recursive process, especially since std::random_access_iterator builds on top of 4 other iterator types. Since this is a C++20 question, I aim to use C++20 features as much as possible (since it greatly simplifies the implementation). This is the complete iterator implementation:

    template<typename T, int SIZE>
    class Array
    {
    public:
        class Iterator
        {
        public:
            using iterator_category = std::contiguous_iterator_tag;
            using iterator_concept  = std::contiguous_iterator_tag;
            //using difference_type   = std::ptrdiff_t; // Likely the same
            using difference_type = typename std::iterator<
                std::contiguous_iterator_tag, T>::difference_type;
            //using value_type        = T;
            using value_type        = std::remove_cv_t<T>; // Using `T` seems sufficient
            using pointer           = T*;
            using reference         = T&;
    
            // constructor for Array<T,S>::begin() and Array<T,S>::end()
            Iterator(pointer ptr) : mPtr(ptr) {}
    
            // std::weakly_incrementable<I>
            Iterator& operator++() { ++mPtr; return *this; }
            Iterator operator++(int) { Iterator tmp = *this; ++(*this); return tmp; }
            Iterator() : mPtr(nullptr/*&mArray[0]*/) {} // TODO: Unsure which is correct!
    
            // std::input_or_output_iterator<I>
            reference operator*() { return *mPtr; }
    
            // std::indirectly_readable<I>
            friend reference operator*(const Iterator& it) { return *(it.mPtr); }
    
            // std::input_iterator<I>
            // No actions were needed here!
    
            // std::forward_iterator<I>
            // In C++20, 'operator==' implies 'operator!='
            bool operator==(const Iterator& it) const { return mPtr == it.mPtr; }
    
            // std::bidirectional_iterator<I>
            Iterator& operator--() { --mPtr; return *this; }
            Iterator operator--(int) { Iterator tmp = *this; --(*this); return tmp; }
    
            // std::random_access_iterator<I>
            //     std::totally_ordered<I>
            std::weak_ordering operator<=>(const Iterator& it) const {
                return std::compare_three_way{}(mPtr, it.mPtr);
                // alternatively: `return mPtr <=> it.mPtr;`
            }
            //     std::sized_sentinel_for<I, I>
            difference_type operator-(const Iterator& it) const { return mPtr - it.mPtr; }
            //     std::iter_difference<I> operators
            Iterator& operator+=(difference_type diff) { mPtr += diff; return *this; }
            Iterator& operator-=(difference_type diff) { mPtr -= diff; return *this; }
            Iterator operator+(difference_type diff) const { return Iterator(mPtr + diff); }
            Iterator operator-(difference_type diff) const { return Iterator(mPtr - diff); }
            friend Iterator operator+(difference_type diff, const Iterator& it) {
                return it + diff;
            }
            friend Iterator operator-(difference_type diff, const Iterator& it) {
                return it - diff;
            }
            reference operator[](difference_type diff) const { return mPtr[diff]; }
    
            // std::contiguous_iterator<I>
            pointer operator->() const { return mPtr; }
            using element_type      = T;
    
        private:
            T* mPtr;
        };
    
        // === STATIC ASSERTS ===
        // - to verify correct Iterator implementation!
        static_assert(std::weakly_incrementable<Iterator>);
        static_assert(std::input_or_output_iterator<Iterator>);
        static_assert(std::indirectly_readable<Iterator>);
        static_assert(std::input_iterator<Iterator>);
        static_assert(std::incrementable<Iterator>);
        static_assert(std::forward_iterator<Iterator>);
        static_assert(std::bidirectional_iterator<Iterator>);
        static_assert(std::totally_ordered<Iterator>);
        static_assert(std::sized_sentinel_for<Iterator, Iterator>);
        static_assert(std::random_access_iterator<Iterator>);
        static_assert(std::is_lvalue_reference_v<std::iter_reference_t<Iterator>>);
        static_assert(std::same_as<std::iter_value_t<Iterator>,
                      std::remove_cvref_t<std::iter_reference_t<Iterator>>>);
        static_assert(std::contiguous_iterator<Iterator>);
    
    
        const T& operator[](int i) const {
            if (i < 0 || i >= SIZE) {
                throw std::runtime_error("Array index out of bounds");
            }
            return mArray[i];
        }
    
        T& operator[](int i) {
            if (i < 0 || i >= SIZE) {
                throw std::runtime_error("Array index out of bounds");
            }
            return mArray[i];
        }
    
        Iterator begin()  { return Iterator(&mArray[0]); }
        Iterator end()  { return Iterator(&mArray[SIZE]); }
    private:
        T mArray[SIZE];
    };
    
    // Check that the Array class can be used as a contiguous_range.
    static_assert(std::ranges::contiguous_range<Array<int, 10>>);
    

    NOTE: using element_type = T; was necessary because of a bug in the specification, which might be fixed. I found information about that here. Adding this fixed issue with std::to_address<Iterator> not being able to compile, and was the last missing piece in going from std::random_access_iterator to std::contiguous_iterator.

    Testing

    I did not perform a complete testing suite with all algorithms, but I chose a few which depend on ranges and std::random_access_iterator. It all runs smoothly. I also depend on building standard library headers as module units, because I want to showcase how C++20 features work together.

    import <stdexcept>;
    import <iostream>;
    import <iterator>;
    import <algorithm>;
    import <random>;
    #include <memory>    // fails to build header unit!
    
    template<typename T, int SIZE>
    class Array
    {
        [...]
    };
    
    int main()
    {
        Array<int, 10> arr;
        for (int i = 0; i < 10; i++) arr[i] = i;
        // I need to call std::ragnes::shuffle since that depends on
        // std::random_access_iterator, so that is a minimum.
        // https://en.cppreference.com/w/cpp/algorithm/ranges/shuffle
        std::random_device rd;
        std::mt19937 gen{rd()};
        std::cout << "before random shuffle:\n";
        for (auto& i : arr) std::cout << i << ' ';
        std::ranges::shuffle(arr, gen);
        std::cout << "\nafter random shuffle:\n";
        for (auto& i : arr) std::cout << i << ' ';
        std::cout << '\n';
    
        // Also std::ranges::stable_sort is a good check (also random_access_iterator):
        // https://en.cppreference.com/w/cpp/algorithm/ranges/stable_sort
        std::cout << "after stable_sort:\n";
        std::ranges::stable_sort(arr);
        for (auto& i : arr) std::cout << i << ' ';
        std::cout << '\n';
    
        auto [min,max] = std::ranges::minmax(arr);
        std::cout << "min: " << min << ", max: " << max << '\n';
    
        return 0;
    }