Nathan29006781
Nathan29006781

Reputation: 335

Construct iterator from const_iterator?

I am writing a container class and have created a nested iterator class. Now I am putting in the const_iterator class. Other than a few typedefs, the implementation is near-identical. Do I really have to have the same code twice, or is there a way to have one class use the other's implementation?

Currently I am giving the iterator class a private constructor like so:

explicit iterator(const_iterator& it): internal{const_cast<pointer>(it.internal)}, begin{const_cast<pointer>(it.begin)}, end{const_cast<pointer>(it.end)}, cycle{it.cycle} {};

So in the const_iterator class, rather than rewrite the code, I have the following

const_iterator& operator+=(difference_type n) {return *this = iterator{*this} += n;}

I understand that this is technically not safe, but since users wouldn't be able to convert const_iterator to iterator is it ok?

A side question: If I go this route and have iterator constructible from const_iterator, should it be an operator or constructor?

Upvotes: 1

Views: 138

Answers (1)

LoS
LoS

Reputation: 1835

Since C++20, an array iterator can be implemented in the following way.

Interface

template <typename T, typename Ptr>
struct VectorIterator
{
  using value_type        = T;
  using pointer           = Ptr;
  using reference         = std::iter_reference_t<Ptr>;
  using difference_type   = std::ptrdiff_t;
  using iterator_category = std::contiguous_iterator_tag;

  VectorIterator();
  explicit VectorIterator(pointer);

  template <std::convertible_to<Ptr> PtrR>
  VectorIterator(const VectorIterator<T, PtrR>&);

  reference operator*() const;
  pointer operator->() const;
  reference operator[](difference_type) const;

  VectorIterator& operator++();
  VectorIterator operator++(int);

  VectorIterator& operator--();
  VectorIterator operator--(int);

  VectorIterator& operator+=(difference_type);
  VectorIterator& operator-=(difference_type);
};

template <typename T, typename PtrL, typename PtrR>
bool operator==(const VectorIterator<T, PtrL>&, const VectorIterator<T, PtrR>&);

template <typename T, typename PtrL, typename PtrR>
auto operator<=>(const VectorIterator<T, PtrL>&, const VectorIterator<T, PtrR>&);

template <typename T, typename Ptr>
VectorIterator<T, Ptr> operator+(const VectorIterator<T, Ptr>&, VectorIterator<T, Ptr>::difference_type);

template <typename T, typename Ptr>
VectorIterator<T, Ptr> operator+(VectorIterator<T, Ptr>::difference_type, const VectorIterator<T, Ptr>&);

template <typename T, typename Ptr>
VectorIterator<T, Ptr> operator-(const VectorIterator<T, Ptr>&, VectorIterator<T, Ptr>::difference_type);

template <typename T, typename PtrL, typename PtrR>
auto operator-(const VectorIterator<T, PtrL>&, const VectorIterator<T, PtrR>&);

Design choices

1. iterator / const_iterator

The first aspect of the iterator concerns the implementation of iterator and const-iterator. Although these can be implemented in two different classes, the approach requires duplication of much of the code, since the only difference between the two interfaces concerns the const-qualification of the pointer and reference types.

A common design is to create a single class that adds a boolean template parameter, which allows to specify whether the iterator should be const-qualified. In this way, the const-qualified version of the pointer and reference types, and possibly other internal types, is selected through a meta-programmaing switch. For simplicity, this can be done with a standard type trait, std::conditional.

The VectorIterator class takes an alternative of the previous approach, replacing the boolean parameter with a Ptr template parameter. It allows the pointer type to be specified, while the reference type is deduced from the previous one. In this way, the original const-qualification is maintained: if the template parameter Ptr is a pointer to const T, the class will be of type const-iterator; otherwise, the class will be of type iterator.

2. Fancy pointers

The technique is also designed to support fancy pointers. Indeed, the template parameter Ptr allows to specify any type that model both NullablePointer and LegacyRandomAccessIterator.

These are the general requirements that the Standard imposes for a type to be called a fancy pointer. However, each iterator may have less strict requirements, covering only supported operations.

To handle fancy pointers properly, the class works through the std::pointer_traits interface and std::iter_* group of aliases. It is important to note that, to truly allow the support of fancy pointers, the implementation must appropriately use certain functions, like pointer_to(), which creates a fancy pointer from the reference to an already-existing object, and std::to_address(), which returns a raw pointer representing the same address as the fancy pointer does.

3. Conversion constructor

The conversion constructor is the most commonly used approach to allow implicit conversion from iterator to const-iterator. Specifically, the const-iterator class must declare an extended copy constructor that accepts an iterator as an argument. Since the VectorIterator class may represent both the iterator and const-iterator, the conversion constructor must be declared as a function that takes an instance of the class with a different pointer type.

template <typename PtrR>
VectorIterator(const VectorIterator<T, PtrR>&);

This declaration seems robust, but it may leads to subtle bugs. Indeed, it is not possible to determine at compile-time whether a VectorIterator specialization is convertible to another: if the above is tested via a type trait, such as std::is_convertible, the result will be positive even if the operation is not really possible. This occurs because the type trait verifies only whether a certain expression, which in this case is the implicit construction, is well-formed. To summarize, since the constructor takes unconditionally an instance of the class for any different pointer type, the check will be always successful.

To resolve the issue, it is sufficient to constraint the constructor. In particular, only pointer types that are implicitly convertible to the current pointer type are accepted.

template <std::convertible_to<Ptr> PtrR>
VectorIterator(const VectorIterator<T, PtrR>&);

Upvotes: 0

Related Questions