Как избежать дублирования кода, реализующего итераторы const и non-const?

я реализую пользовательский контейнер с интерфейсом, подобным STL. Я должен предоставить обычный итератор и итератор const. Большая часть кода для двух версий итераторов идентична . Как избежать этого дублирования?

например, мой класс контейнера Foo и я реализации FooIterator и FooConstIterator. Оба итератора должны предоставлять такие методы, как operator++(), которые идентичны.

мой вопрос похож на как я удалить дублирование кода между аналогичными функциями-членами const и non-const?, но ответ на этот вопрос специфичен для методов const и non-const, особенно аксессоров. Я не вижу, как это может обобщить проблему итератора.

Я должен FooIterator выводим из FooConstIterator и расширить его с помощью дополнительных методов non-const? Это приводит либо к виртуальным методам, либо к скрытию методов, которые здесь кажутся неуместными.

возможно FooIterator должен содержать FooConstIterator. Хотя этот подход сокращает дублирование реализации, он, по-видимому, повторно вводит много стандартных определений методов.

есть ли умный шаблон для генерации двух итераторов из одного определения? Или, возможно, есть способ-содрогнуться-использовать препроцессор, чтобы уничтожить эти почти идентичные классы.

Я попытался посмотреть на мою локальную реализацию STL, чтобы увидеть, как она справляется с этим. Есть так много вспомогательных классов, которые я возникли проблемы с дизайном, но похоже, что функциональность просто дублируется.

в предыдущих проектах мой пользовательский контейнер был построен поверх стандартного контейнера STL, поэтому мне не нужно было предоставлять свои собственные итераторы. В данном случае это не вариант.

5 ответов


[лучший ответ, К сожалению, был удален модератором, потому что это был ответ только по ссылке. Я понимаю, почему ответы только на ссылки не поощряются; удаление его, однако, лишило будущих искателей очень полезной информации. Связь остается стабильной на протяжении более семи лет и продолжает работать на момент написания этой статьи.]

я настоятельно рекомендую оригинальную журнальную статью доктора Добба Мэтта Остерна под названием "Стандартные Библиотекарь: Определение итераторов и итераторов Const" в январе 2001 года. Если эта ссылка пойдет плохо, теперь, когда доктор Добб перестал работать, она также доступна здесь.

чтобы предотвратить удаление этого ответа на замену, я обобщу решение.

идея состоит в том, чтобы реализовать итератор один раз в качестве шаблона, который принимает дополнительный параметр шаблона, логическое значение, которое говорит, является ли это версией const. В любом месте реализации, где версии const и non-const различаются, для выбора правильного кода используется механизм шаблона. Механизм Мэтта Остерна назывался choose. Выглядело это так:--9-->

template <bool flag, class IsTrue, class IsFalse>
struct choose;

template <class IsTrue, class IsFalse>
struct choose<true, IsTrue, IsFalse> {
   typedef IsTrue type;
};

template <class IsTrue, class IsFalse>
struct choose<false, IsTrue, IsFalse> {
   typedef IsFalse type;
};

если бы у вас были отдельные реализации для итераторов const и non-const, то реализация const включала бы следующие типы:

typedef const T &reference;
typedef const T *pointer;

и реализация non-const будет иметь:

typedef T &reference;
typedef T *pointer;

но с choose, вы можете иметь одну реализацию, которая выбирает на основе на дополнительном параметре шаблона:

typedef typename choose<is_const, const T &, T &>::type reference;
typedef typename choose<is_const, const T *, T *>::type pointer;

используя typedefs для базовых типов, все методы итератора могут иметь идентичную реализацию. Смотрите Мэтт Austern по пример.


поскольку C++11/14 вы можете избежать таких маленьких помощников, вывести константу непосредственно из логического шаблона.

constness.h:

#ifndef ITERATOR_H
#define ITERATOR_H
#include <cstddef>
#include <cstdint>
#include <type_traits>
#include <iterator>

struct dummy_struct {
  int hello = 1;
  int world = 2;
  dummy_struct() : hello{ 0 }, world{ 1 }{ }
};

template< class T >
class iterable {
  public:
    template< bool Const = false >
    class my_iterator {
      public:
        using iterator_category = std::forward_iterator_tag;
        using value_type = T;
        using difference_type = std::ptrdiff_t;
        /* deduce const qualifier from bool Const parameter */
        using reference = typename std::conditional_t< Const, T const &, T & >;
        using pointer = typename std::conditional_t< Const, T const *, T * >;

      protected:
        pointer i;

      public:
        my_iterator( T* _i ) : i{ reinterpret_cast< pointer >( _i ) } { }

        /* SFINAE enables the const dereference operator or the non 
           const variant
           depending on bool Const parameter */          
        template< bool _Const = Const >
        std::enable_if_t< _Const, reference >
        operator*() const {
          std::cout << "Const operator*: ";
          return *i;
        }

        template< bool _Const = Const >
        std::enable_if_t< !_Const, reference >
        operator*() {
          std::cout << "Non-Const operator*: ";
          return *i; 
        }

        my_iterator & operator++() {
          ++i;
          return *this;
        }
        bool operator!=( my_iterator const & _other ) const {
          return i != _other.i;
        }

        bool operator==( my_iterator const & _other ) const {
          return !( *this != _other );
        }   
    };  



  private:
    T* __begin;
    T* __end; 
  public:
    explicit iterable( T* _begin, std::size_t _count ): __begin{ _begin }, __end{ _begin + _count } { std::cout << "End: " << __end << "\n"; }

    auto begin()  const { return my_iterator< false >{ __begin }; }
    auto end()    const { return my_iterator< false >{ __end }; }

    auto cbegin() const { return my_iterator< true >{ __begin }; }
    auto cend()   const { return my_iterator< true >{ __end }; }
};
#endif

Это можно использовать С что-то вроде этого:

#include <iostream>
#include <array>
#include "constness.h"

int main() {

  dummy_struct * data = new dummy_struct[ 5 ];
  for( int i = 0; i < 5; ++i ) {
    data[i].hello = i;
    data[i].world = i+1;
  } 
  iterable< dummy_struct > i( data, 5 );

  using iter = typename iterable< dummy_struct >::my_iterator< false >;
  using citer = typename iterable< dummy_struct >::my_iterator< true >;

  for( iter it = i.begin(); it != i.end(); ++it  ) {
    std::cout << "Hello: " << (*it).hello << "\n"
              << "World: " << (*it).world << "\n";
  }

  for( citer it = i.cbegin(); it != i.cend(); ++it  ) {
    std::cout << "Hello: " << (*it).hello << "\n"
              << "World: " << (*it).world << "\n";
  }
  delete[] data;

}

STL использует наследование

template<class _Myvec>
    class _Vector_iterator
        : public _Vector_const_iterator<_Myvec>

в дополнение к предложению о том, что вы можете шаблонизировать константу и не-константу, вы также можете уменьшить объем работы, взглянув на импульс.Итератор учебник - который также упоминает то же самое решение.


вы можете использовать CRTP и общую базу для "инъекции" методов (но вам все равно нужно дублировать ctors в текущем C++) или просто использовать препроцессор (не требуется дрожание; легко обрабатывает ctors):

struct Container {

#define G(This) \
This operator++(int) { This copy (*this); ++*this; return copy; }
// example of postfix++ delegating to ++prefix

  struct iterator : std::iterator<...> {
    iterator& operator++();
    G(iterator)
  };
  struct const_iterator : std::iterator<...> {
    const_iterator& operator++();
    G(const_iterator)
  };

#undef G
// G is "nicely" scoped and treated as an implementation detail
};

используйте std:: iterator, typedefs, которые он дает вам, и любые другие typedefs, которые вы можете предоставить, чтобы сделать макрос прямым.