Author: | David Abrahams |
---|---|
Contact: | dave@boost-consulting.com |
Organization: | Boost Consulting |
date: | $Date$ |
Copyright: | Copyright David Abrahams 2003. Use, modification and distribution is subject to the Boost Software License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) |
Several issues with N1550 (New Iterator Concepts) were raised in the run-up before the fall 2003 C++ Committee meeting, in a thread beginning with John Maddock's posting c++std-lib-12187. In looking at those issues, several other problems came up. This document addresses those issues and discusses some potential solutions and their impact on N1530 (Iterator Facade and Adaptor).
The proposed iterator_tag class template accepts an "access bits" parameter which includes a bit to indicate the iterator's lvalueness (whether its dereference operator returns a reference to its value_type. The relevant part of N1550 says:
The purpose of the lvalue_iterator part of the iterator_access enum is to communicate to iterator_tag whether the reference type is an lvalue so that the appropriate old category can be chosen for the base class. The lvalue_iterator bit is not recorded in the iterator_tag::access data member.
The lvalue_iterator bit is not recorded because N1550 aims to improve orthogonality of the iterator concepts, and a new-style iterator's lvalueness is detectable by examining its reference type. This inside/outside difference is awkward and confusing.
Shortly after N1550 was accepted, we discovered that an iterator's lvalueness can be determined knowing only its value_type. This predicate can be calculated even for old-style iterators (on whose reference type the standard places few requirements). A trait in the Boost iterator library does it by relying on the compiler's unwillingness to bind an rvalue to a T& function template parameter. Similarly, it is possible to detect an iterator's readability knowing only its value_type. Thus, any interface which asks the user to explicitly describe an iterator's lvalue-ness or readability seems to introduce needless complexity.
The part of the is_writable_iterator trait definition which applies to old-style iterators is:
if (cat is convertible to output_iterator_tag) return true; else if ( cat is convertible to forward_iterator_tag and iterator_traits<Iterator>::reference is a mutable reference) return true; else return false;
The current forward iterator requirements place no constraints on the iterator's reference type, so the logic above will give false negatives for some otherwise-writable forward iterators whose reference type is not a mutable reference. Also, it will report false positives for any forward, bidirectional, or random access iterator whose reference is a mutable reference but whose value_type is not assignable (e.g. has a private assignment operator).
Similarly, the part of is_swappable_iterator which applies to old-style iterators is:
else if (cat is convertible to forward_iterator_tag) { if (iterator_traits<Iterator>::reference is a const reference) return false; else return true; } else return false;
In this case false positives are possible for non-writable forward iterators whose reference type is not a reference, or as above, any forward, bidirectional, or random access iterator whose reference is not a constant reference but whose value_type is not assignable (e.g., because it has a private assignment operator).
False negatives can be "reasoned away": since it is part of a writable iterator's concept definition that is_writable<I>::value is true, any iterator for which it is false is by definition not writable. This seems like a perverse use of logic, though.
It might be reasonable to conclude that it is a defect that the standard allows forward iterators with a reference type other than value_type cv&, but that still leaves the problem of old-style iterators whose value_type is not assignable. It is not possible to correctly compute writability and swappability for those old-style iterators without intervention (specializations of is_writable_iterator and is_swappable_iterator) from a user.
is_swappable_iterator<I> is supposed to yield true if iter_swap(x,y) is valid for instances x and y of type I. The only argument we have heard for is_swappable_iterator goes something like this:
"If is_swappable_iterator yields false, you could fall back to using copy construction and assignment on the value_type instead."
This line of reasoning, however, falls down when closely examined. To achieve the same effect using copy construction and assignment on the iterator's value_type, the iterator must be readable and writable, and its value_type must be copy-constructible. But then, iter_swap must work in that case, because its default implementation just calls swap on the dereferenced iterators. The only purpose for the swappable iterator concept is to represent iterators which do not fulfill the properties listed above, but which are nonetheless swappable because the user has provided an overload or specialization of iter_swap. In other words, generic code which wants to swap the referents of two iterators should always call iter_swap instead of doing the assignments.
Try to imagine a case where is_writable_iterator can be used to choose behavior. Since the only requirement on a writable iterator is that we can assign into its referent, the only use for is_writable_iterator in selecting behavior is to modify a sequence when the sequence is mutable, and to not modify it otherwise.
There is no precedent for generic functions which modify their arguments only if the arguments are non-const reference, and with good reason: the simple fact that data is mutable does not mean that a user intends it to be mutated. We provide const and non-const overloads for functions like operator[], but these do not modify data; they merely return a reference to data which preserves the object's mutability properties. We can do the same with iterators using their reference types; the accessibility of an assignment operator on the value_type, which determines writability, does not change that.
The one plausible argument we can imagine for is_writable_iterator and is_swappable_iterator is that they can be used to remove algorithms from an overload set using a SFINAE technique like enable_if, thus minimizing unintentional matches due to Koenig Lookup. If it means requiring explicit indications of writability and swappability from new-style iterator implementors, however, it seems to be too small a gain to be worth the cost. That's especially true since we can't get many existing old-style iterators to meet the same requirements.
Howard Hinnant pointed out some inconsistencies with the naming of these tag types:
incrementable_iterator_tag // ++r, r++ single_pass_iterator_tag // adds a == b, a != b forward_traversal_iterator_tag // adds multi-pass capability bidirectional_traversal_iterator_tag // adds --r, r-- random_access_traversal_iterator_tag // adds r+n,n+r,r-n,r[n],etc.
Howard thought that it might be better if all tag names contained the word "traversal".
It's not clear that would result in the best possible names, though. For example, incrementable iterators can only make a single pass over their input. What really distinguishes single pass iterators from incrementable iterators is not that they can make a single pass, but that they are equality comparable. Forward traversal iterators really distinguish themselves by introducing multi-pass capability. Without entering a "Parkinson's Bicycle Shed" type of discussion, it might be worth giving the names of these tags (and the associated concepts) some extra attention.
The names is_readable, is_writable, and is_swappable are probably too general for their semantics. In particular, a swappable iterator is only swappable in the same sense that a mutable iterator is mutable: the trait refers to the iterator's referent. It would probably be better to add the _iterator suffix to each of these names.
We believe that is_readable_iterator is a fine name for the proposed is_readable trait and will use that from here on. In order to avoid confusion, however, and because we aren't terribly convinced of any answer yet, we are going to phrase this solution in terms of the existing traversal concept and tag names. We'll propose a few possible traversal naming schemes at the end of this section.
Following the dictum that what we can't do well probably shouldn't be done at all, we'd like to solve many of the problems above by eliminating details and simplifying the library as proposed. In particular, we'd eliminate is_writable and is_swappable, and remove the requirements which say that writable, and swappable iterators must support these traits. is_readable_iterator has proven to be useful and will be retained, but since it can be implemented with no special hints from the iterator, it will not be mentioned in the readable iterator requirements. Since we don't want to require the user to explicitly specify access category information, we'll change iterator_tag so that it computes the old-style category in terms of the iterator's traversal category, reference, and value_type.
A cleaner solution would change iterator_traits as follows, though this does not constitute a "pure bolt-on":
iterator_traits<I>::iterator_category = if (I::iterator_category is a type) // use mpl::has_xxx (SFINAE) return I::iterator_category if (iterator_value_type<I>::type is void || iterator_difference_type<I>::type is void ) return std::output_iterator_tag t = iterator_traversal<I>::type if (I is an lvalue iterator) { if (t is convertible to random_access_traversal_tag) return std::random_access_iterator_tag if (t is convertible to bidirectional_traversal_tag) return std::bidirectional_iterator_tag else if (t is convertible to forward_traversal_tag) return std::forward_iterator_tag } if (t is convertible to single_pass_traversal_tag && I is a readable iterator ) return input_output_iterator_tag // (**) else return std::output_iterator_tag