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

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

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

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

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

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

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

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

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

Ответ 1

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

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

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

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

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 включала бы typedefs следующим образом:

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

а реализация не-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;

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

Ответ 2

Начиная с С++ 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;

}

Ответ 3

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

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

Ответ 4

В дополнение к предположению о том, что вы можете templatize constness и non-constness, вы также можете уменьшить объем работы, взглянув на Boost. Iterator, в котором также упоминается одно и то же решение.

Ответ 5

Вы можете использовать CRTP и общую базу для "инъекции" методов (но вам все равно придется дублировать 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, который он дает вам, и любые другие типы typedef, которые вы могли бы предоставить, чтобы сделать макрос прямолинейным.

Ответ 6

Arthor O'Dwyer подробно отвечает на это в своем блоге: https://quuxplusone.github.io/blog/2018/12/01/const-iterator-antipatterns/

По сути,

template<bool IsConst>
class MyIterator {
    int *d_;
public:
    MyIterator(const MyIterator&) = default;  // REDUNDANT BUT GOOD STYLE

    template<bool IsConst_ = IsConst, class = std::enable_if_t<IsConst_>>
    MyIterator(const MyIterator<false>& rhs) : d_(rhs.d_) {}  // OK
};
using Iterator = MyIterator<false>;
using ConstIterator = MyIterator<true>;
};

Также добавьте static_assert(std::is_trivially_copy_constructible_v<ConstIterator>); в ваш код, чтобы убедиться, что ваши итераторы остаются тривиально копируемыми:

Вывод: если вы реализуете свои собственные итераторы контейнеров - или любую другую пару типов с этим поведением "одностороннего неявного преобразования", например, TS сетевых служб const_buffers_type и mutable_buffers_type - тогда вам следует использовать один из приведенных выше шаблонов для реализации конструкторов преобразования без случайного отключения тривиального копирования.