问题
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:
Are there any problems with the first version?
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.
回答1:
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.
来源:https://stackoverflow.com/questions/58661203/what-is-the-correct-way-to-implement-iterator-and-const-iterator-in-c17