7

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.

4
  • Not an answer to the specific question, but note that boost::iterator_facade and boost::iterator_adaptor help with a lot of the required boilerplate for creating iterator types.
    – aschepler
    Commented Nov 1, 2019 at 14:51
  • Also, besides "in C++17", how is this question not a duplicate of the one you linked to? Commented Nov 1, 2019 at 14:57
  • @NicolBolas thanks for the clarification, I edited my code to reflect this. I actually got this from the implementation of std::array::iterator and thought it would be named like this appear in intellisense only at the end. As kind of "almost hidden" class or such.
    – Lukas-T
    Commented Nov 1, 2019 at 15:07
  • @NicolBolas The question aims more at the reason, why the second implementation should be preferred (should it?). Beause I fail to see, why the first solution would have become invalid or impracticle in c++17.
    – Lukas-T
    Commented Nov 1, 2019 at 15:09

1 Answer 1

3

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.

7
  • Why should iterator be trivial copyable? At cppreference page (en.cppreference.com/w/cpp/named_req/BidirectionalIterator) about iterators, I've not found such named requirement.
    – wcobalt
    Commented Nov 1, 2019 at 16:42
  • @wcobalt: Why shouldn't it be trivially copyable if it can be? Yes, there's no explicit requirement of it, but that doesn't mean you should throw away the possibility either. Especially when providing it is pretty simple. Commented Nov 1, 2019 at 16:56
  • Sorry, but I do not understand for what you need was_const. Could you explain it?
    – wcobalt
    Commented Nov 1, 2019 at 17:34
  • @wcobalt: I don't know that there is a need for was_const; I was simply copying the code from the link. My knowledge and understanding of SFINAE is limited, so I just generally assume that if a template parameter is there, then it needs to be for some reason. Commented Nov 1, 2019 at 19:15
  • SFINAE only works on member functions with the function's own template parameters, not its class's template parameters. That's why an additional parameter is needed, though it won't work in your code because it's not actually being use in the enable_if. However, in the blog you linked, WasConst is deduced from the function argument, and is used because apparently the usual trick doesn't work with non-GCC compilers (news to me -- maybe it's old compilers only). Commented Nov 1, 2019 at 20:15

Not the answer you're looking for? Browse other questions tagged or ask your own question.