Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the correct way to implement iterator and const_iterator in C++17?

Tags:

c++

c++17

When implementing a custom container I came to the point where I needed to implement iterators. Of course I didn't want to write the code twice for const and non-const iterator. I found this question detailing a possible implementation like this:

template<class T>
class ContainerIterator {
    using pointer = T*;
    using reference = T&;
    ...
};

template<class T>
class Container {
    using iterator_type = ContainerIterator<T>;
    using const_iterator_type = ContainerIterator<const T>;
}

But I also found this this question that uses a template parameter:

template<class T, bool IsConst>
class ContainerIterator {
    using pointer = std::conditional_t<IsConst, const T*, T*>;
    using reference = std::conditional_t<IsConst, const T&, T&>;
    ...
};

template<class T>
class Container {
    using iterator_type = ContainerIterator<T, false>;
    using const_iterator_type = ContainerIterator<T, true>;
}

The first solution seems to be easier, but the answer is from 2010. After doing some research, it seems, that the first version is not widely used, but I can't see why. I feel like I'm missing some obvious flaw of the first version.


So the questions become:

  1. Are there any problems with the first version?

  2. If no, why version #2 seems to be the preferred way in c++17? Or why should I prefer one over the other?


Also, yes, it would be a solution to use const_cast or simply duplicate the whole code. But I don't like those two.
like image 891
churill Avatar asked Nov 01 '19 14:11

churill


People also ask

What is a const iterator in C++?

A const iterator points to an element of constant type which means the element which is being pointed to by a const_iterator can’t be modified. Though we can still update the iterator (i.e., the iterator can be incremented or decremented but the element it points to can not be changed).

How to insert an element in an iterator while iterating?

But we can make a few simple modifications to the iterator class so that we could also insert an element while we iterate: We have to add LinkedList as a friend class to the Iterator so that it can access its private members: friend class LinkedList and remove const from Iterator::previous_node and Iterator::current_node.

What is the iterator pattern?

Now, what if we build an iterator that provides a generic way of iterating over a collection independent of its type. Iterator pattern lets us do just that. Formally it is defined as below: The iterator pattern provides a way to access the elements of an aggregate object without exposing its underlying representation.

Where should the iterator point?

The resulting iterator should point at the same spot as the original does. Show activity on this post. Containers are required to provide iterator as a type convertible to const_iterator, so you can convert implicitly:


1 Answers

The point of the second implementation is something you didn't copy: to make it easy to implement a specific requirement. Namely, an iterator must be implicitly convertible to a const_iterator.

The difficulty here lies in preserving trivial copyability. If you were to do this:

template<class T>
class ContainerIterator {
    using pointer = T*;
    using reference = T&;
    ...
    ContainerIterator(const ContainerIterator &) = default; //Should be trivially copyable.
    ContainerIterator(const ContainerIterator<const T> &) {...}
};

That doesn't work. const const Typename resolves down to const Typename. So if you instantiate ContainerIterator with a const T, then you now have two constructors that have the same signature, one of which is defaulted. Well, this means that the compiler will ignore your defaulting of the copy constructor and thus use you non-trivial copy constructor implementation.

That's bad.

There are ways to avoid this by using some metaprogramming tools to detect the const-ness of the T, but the easiest way to fix it is to specify the const-ness as a template parameter:

template<class T, bool IsConst>
class ContainerIterator {
    using pointer = std::conditional_t<IsConst, const T*, T*>;
    using reference = std::conditional_t<IsConst, const T&, T&>;
    ...

    ContainerIterator(const ContainerIterator &) = default; //Should be trivially copyable.
    template<bool was_const = IsConst, class = std::enable_if_t<IsConst || !was_const>>>
    ContainerIterator(const ContainerIterator<T, was_const> &) {...}
};

Templates are never considered to be copy constructors, so this won't interfere with trivial copyability. This also uses SFINAE to eliminate the converting constructor in the event that it is not a const iterator.

More information about this pattern can be found as well.

like image 64
Nicol Bolas Avatar answered Sep 22 '22 12:09

Nicol Bolas