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

Creating an Iterator with C++20 Concepts for custom container


C++20 introduces concepts, a smart way to put constraints on the types a template function or class can take in.

While iterator categories and properties remain the same, what changes is how you enforce them: with tags until C++17, with concepts since C++20. For example, instead of the std::forward_iterator_tag tag you would mark your iterator with the std::forward_iterator concept.

The same thing applies to all iterator properties. For example, a Forward Iterator must be std::incrementable. This new mechanism helps in getting better iterator definitions and makes errors from the compiler much more readable.

This piece of text it's taken from this article: https://www.internalpointers.com/post/writing-custom-iterators-modern-cpp

But the author didn't upgrade the content on how to make a custom iterator on C++20 with concepts, it remains the <= C++17 tags version.

Can someone make an example on how to write a custom iterator for a custom container in a C++20 version with the concept features?


Solution

  • By and large, the C++20 way of defining iterators does away with explicitly tagging the type, and instead relies on concepts to just check that a given type happens to respect the iterator category's requirements.

    This means that you can now safely duck-type your way to victory while supporting clean overload resolution and error messages:

    struct my_iterator {
      // No need for tagging or anything special, just implement the required interface.
    };
    

    If you want to ensure that a given type fulfills the requirements of a certain iterator category, you static_assert the concept on that type:

    #include <iterator>
    
    static_assert(std::forward_iterator<my_iterator>);
    

    Enforcing that a function only accepts a certain iterator category is done by using the concept in your template arguments.

    #include <iterator>
    
    template<std::forward_iterator Ite, std::sentinel_for<Ite> Sen>
    void my_algorithm(Ite begin, Sen end) {
     // ...
    }
    

    std::sentinel_for<> is now used for the end iterator instead of using Ite twice. It allows to optionally use a separate type for the end iterator, which is sometimes convenient, especially for input iterators.

    For example:

    struct end_of_stream_t {};
    constexpr end_of_stream_t end_of_stream{};
    
    struct my_input_iterator {
      // N.B. Not a complete implementation, just demonstrating sentinels.
      some_stream_type* data_stream;
    
      bool operator==(end_of_stream_t) const { return data_stream->empty(); }
    };
    
    template<std::input_iterator Ite, std::sentinel_for<Ite> Sen>
    void my_algorithm(Ite begin, Sen end) {
      while(begin != end) {
        //...
      }
    }
    
    void foo(some_stream_type& stream) {
      my_algorithm(my_input_iterator{&stream}, end_of_stream);
    }