When creating a new iterator pre-C++20 without the help of libraries like boost.iterator, it's necessary to specify the type aliases difference_type
, value_type
, pointer
, reference
and iterator_category
.
According to cppreference, with C++20, it's only necessary to specify difference_type
and value_type
, which I think is great!
But why are there defaults for exactly these 3 aliases?
There are 2 things I don't understand about this (and one thing that seems to me like an oversight):
- Why are there no default values for
value_type
anddifference_type
? Wouldn't it make sense to use something likestd::remove_reference_t<reference>
as a default forvalue_type
? As a default fordifference_type
for random access iterators, it could arguably make sense to use the result type of the-
operator taking two iterators. - C++20 adds the
contiguous_iterator_tag
. Just like withinput_iterator_tag
versusforward_iterator_tag
, I don't see how it should be possible for the compiler to correctly distinguish between a contiguous iterator and a random access iterator, which I guess is why it apparently never selectscontiguous_iterator_tag
. Is this intended? It also seems somewhat dangerous to misclassify an input iterator as a forward iterator, so why don't require the programmers to specify this alias themselves? - On a somewhat unrelated note, I'm not sure if it's a good idea to silently generate a value for
iterator_category
even if the programmer has explicitly stated another category, and generating a value foriterator_category
that's completely different from theconcept
seems strange as well. Consider this unrealistic example:
#include <iostream>
#include <iterator>
// With the == operator, this is an input iterator, but nothing else.
struct WeirdIterator {
// Not an output iterator because you can't assign to a const reference
const int& operator*() const { return 42; }
WeirdIterator& operator++() { return *this; } // unimportant
WeirdIterator operator++(int) { return *this; } // unimportant
// bool operator==(const WeirdIterator&) const = default;
using iterator_category = std::random_access_iterator_tag;
using value_type = int;
using difference_type = int;
};
void iteratorConcept(std::input_iterator auto) {
std::cout << "input iterator concept" << std::endl;
}
void iteratorConcept(std::random_access_iterator auto) {
std::cout << "random access iterator concept" << std::endl;
}
void iteratorTag(std::output_iterator_tag) {
std::cout << "output iterator tag" << std::endl;
}
void iteratorTag(std::input_iterator_tag) {
std::cout << "input iterator tag" << std::endl;
}
void iteratorTag(std::random_access_iterator_tag) {
std::cout << "random access iterator tag" << std::endl;
}
int main() {
WeirdIterator iter;
iteratorConcept(iter);
iteratorTag(std::iterator_traits<WeirdIterator>::iterator_category{});
return 0;
}
This prints "input iterator concept" and "output iterator tag" because it's missing the comparison operator (which isn't required for the concept).
If I add the commented line, this now prints "input iterator concept" and "random access iterator tag", even though it clearly isn't a random access iterator. To be fair, writing the wrong iterator_category
(i.e. random_access_iterator_tag
) like this is a pretty stupid example, but I still think it would make sense to check if the concept is satisfied, especially in the case of the "fall-back" output_iterator_tag
: Forgetting to write the ==
operator shouldn't turn an input iterator into an unusable output iterator. Would it be possible and make sense to check that the corresponding concepts are satisfied?
Edit A few points in my question seem to be unclear, or maybe I've made some incorrect but unstated assumptions. I'll try to be more explicit about them and rephrase my current understanding (after reading the answer by Nicol Bolas):
- Regarding Point 3: As I understand it, it's possible that a type
T
may have somestd::iterator_traits<T>::iterator_category
alias even if it doesn't model the corresponding C++20 concept or the C++17 named requirement. This is intended. So, let's forget about this, because it's probably a better fit for a separate question. - I think that the
std::type_traits
aliases defined if I don't explicitly write them down (e.g.reference
when I only writevalue_type
) can be incorrect for some iterators and are meant as sensible default values. Is this correct? If this is incorrect, my question is pretty much answered. - If
T::reference
isn't defined for an input iteratorT
, thenstd::iterator_traits::reference
is defined as decltype(*std::declval<T&>()). Is this correct? - If
reference
can be defined based onoperator*
, wouldn't it make sense to then also definevalue_type
based on*
? Assuming that 5. is correct, the only input iterator I can think of where this would go wrong is the iterator fromstd::vector<bool>
, and there were several proposals to deprecate it because of this difference. So most input iterators would work with this definition, and those that didn't could simply specifyvalue_type
. Am I missing something? - Regarding Point 2: It's not in general decidable into what category an iterator falls.
Using e.g. an input iterator as if it were a more general forward iterator would be a bug. It can happen that the
type_traits::iterator_category
of a valid iterator where the programmer did not specify theiterator_category
is incorrect. This doesn't affect the concept or named requirement (they take semantics into account), but in practical terms, it's possible that stl functions don't work correctly with this iterator, without generating a (run- or compile-time) error. Therefore, I think it would be a good idea to require the programmer to explicitly state the category. Is there a problem in this reasoning or did miss something?
I hope I don't come across as overly pedantic or as insisting on my personal opinion, but I genuinely don't know if and where there's an error in the points above, and I'm guessing that this isn't just confusing to me.