what is the preferred way to expose custom STL-style iteration?

前端 未结 4 1908
春和景丽
春和景丽 2020-12-19 04:47

(also see Is there a good way not to hand-write all twelve required Container functions for a custom type in C++? )


For a class such as

namespa         


        
相关标签:
4条回答
  • 2020-12-19 05:18

    There is a standard which describes what your class interfaces should look like if you want them to be congruent with the STL. C++ has this notion of 'concepts' which pin down the requirements for a given class to be a sufficient implementation of a concept. This almost became a language feature in c++11.

    A concept you may be interested in is the Container concept. As you can see, in order to meet the requirements of the Container concept, you need begin, cbegin, end, and cend as member functions (among other things).

    Since it looks like you're storing your data in an array, you might also be interested in SequenceContainer.

    0 讨论(0)
  • 2020-12-19 05:19

    In order to create a valid iterator, you must ensure that std::iterator_traits is valid. This means you must set the iterator category among other things.

    An iterator should implement iterator(), iterator(iterator&&), iterator(iterator const&), operator==, operator !=, operator++, operator++(int), operator*, operator=, and operator->. It's also a good idea to add operator< and operator+ if you can (you can't always, e.g. linked lists.)

    template <typename T>
    class foo
    {
    public:
    
      using value_type = T;
      class iterator 
      { 
      public:
        using value_type = foo::value_type;
        using iterator_category = std::random_access_iterator_tag;
        // or whatever type of iterator you have...
        using pointer = value_type*;
        using reference = value_type&;
        using difference_type = std::ptrdiff_t;
    
        // ... 
      };
    
      class const_iterator 
      {
        // ... 
      };
    
      iterator begin() { /*...*/ }
      iterator end() { /*...*/ }
    
      const_iterator cbegin() const { /*...*/ }
      const_iterator cend() const { /*...*/ }
    
      /* ... */
    };
    

    See: http://en.cppreference.com/w/cpp/iterator/iterator_traits for more information on what you need to make a valid iterator. (Note: You also need certain properties to be a valid "container", like .size())

    Ideally you should use member functions for begin and end, but it's not required... you can also overload std::begin and std::end. If you don't know how to do that, I suggest you use member functions.

    You should create begin() const and end() const, but it should be an alias for cbegin(), NEVER the same as begin()!

    0 讨论(0)
  • 2020-12-19 05:21

    I suggest creating both sets of functions -- member functions as well as non-member functions -- to allow for maximum flexibility.

    namespace JDanielSmith {
       class C
       {
          const size_t _size;
          const std::unique_ptr<int[]> _data;
    
          public:
          C(size_t size) : _size(size), _data(new int[size]) {}
    
          inline const int* get() const { return _data.get(); }
          inline int* get() { return _data.get(); }
    
          size_t size() const { return _size; }
    
          int* begin() { return get(); }
          int* end() { return get() + _size; }
          const int* begin() const { return get(); }
          const int* end() const { return get() + _size; }
          const int* cbegin() const { return get(); }
          const int* cend() const { return get() + _size; }
    
       };
    
       int* begin(C& c) { return c.begin(); }
       int* end(C& c) { return c.end(); }
       const int* begin(C const& c) { return c.begin(); }
       const int* end(C const& c) { return c.end(); }
       const int* cbegin(C const& c) { return c.begin(); }
       const int* cend(C const& c) { return c.end(); }
    }
    

    The member functions are necessary if you want to be able to use objects of type C as arguments to std::begin, std::end, std::cbegin, and std::cend.

    The non-member functions are necessary if you want to be able to use objects of type C as arguments to just begin, end, cbegin, and cend. ADL will make sure that the non-member functions will be found for such usages.

    int main()
    {
       JDanielSmith::C c1(10);
    
       {
          // Non-const member functions are found
          auto b = std::begin(c1);
          auto e = std::end(c1);
          for (int i = 0; b != e; ++b, ++i )
          {
             *b = i*10;
          }
       }
    
       JDanielSmith::C const& c2 = c1;
       {
          // Const member functions are found
          auto b = std::begin(c2);
          auto e = std::end(c2);
          for ( ; b != e; ++b )
          {
             std::cout << *b << std::endl;
          }
       }
    
       {
          // Non-member functions with const-objects as argument are found
          auto b = begin(c2);
          auto e = end(c2);
          for ( ; b != e; ++b )
          {
             std::cout << *b << std::endl;
          }
       }
    }
    
    0 讨论(0)
  • 2020-12-19 05:22

    I'll take option C.

    The main problem here is that std::begin() doesn't actually work for finding non-member begin() with ADL. So the real solution is to write your own that does:

    namespace details {
        using std::begin;
    
        template <class C>
        constexpr auto adl_begin(C& c) noexcept(noexcept(begin(c)))
            -> decltype(begin(c))
        {
            return begin(c);
        }
    }
    
    using details::adl_begin;
    

    Now, it doesn't matter if you write your begin() as a member or non-member function, just use adl_begin(x) everywhere and it'll just work. As well as for both the standard containers and raw arrays. This conveniently side-steps the member vs. non-member discussion.


    And yes, you should have const and non-const overloads of begin() and friends, if you want to expose const and non-const access.

    0 讨论(0)
提交回复
热议问题