Reputation: 335
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
Reputation: 1835
Since C++20, an array iterator can be implemented in the following way.
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>&);
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.
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.
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