Как избежать дублирования кода, реализующего константные и неконстантные итераторы?
Я реализую пользовательский контейнер с 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 - тогда вам следует использовать один из приведенных выше шаблонов для реализации конструкторов преобразования без случайного отключения тривиального копирования.