+++++++++++++++++++++++++++++++ Issues With N1550_ and N1530_ +++++++++++++++++++++++++++++++ .. _N1550: http://www.boost-consulting.com/writing/n1550.html .. _N1530: http://anubis.dkuug.dk/jtc1/sc22/wg21/docs/papers/2003/n1530.html :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) .. _`Boost Consulting`: http://www.boost-consulting.com .. contents:: Table of Contents ============== Introduction ============== 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 Issues ============ Non-Uniformity of the "``lvalue_iterator`` Bit" =============================================== 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. Redundancy of Some Explicit Access Category Flags ================================================= 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. New Access Traits Templates Wrong For Some Iterators ==================================================== ``is_writable_iterator`` ------------------------ 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::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). ``is_swappable_iterator`` ------------------------- 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::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::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. No Use Cases for Some Access Traits =================================== ``is_swappable_iterator`` ------------------------- ``is_swappable_iterator`` 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. ``is_writable_iterator`` ------------------------ 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. .. _enable_if: http://tinyurl.com/tsr7 Naming Issues ============= Traversal Concepts and Tags --------------------------- 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. Access Traits ------------- 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. ================================ Proposed Solution (in progress) ================================ 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. Overview ======== 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``. Future Enhancements =================== For C++0x, we could consider a change to ``iterator_traits`` which allows the user to avoid the use of iterator_tag (or similar devices) altogether and write a new-style iterator by specifying only a traversal tag. This change is not being proposed as it does not constitute a "pure bolt-on":: iterator_traits::iterator_category = if (I::iterator_category is a type) // use mpl::has_xxx (SFINAE) return I::iterator_category // Only old-style output iterators may have a void value_type // or difference_type if (iterator_value_type::type is void || iterator_difference_type::type is void ) return std::output_iterator_tag t = iterator_traversal::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 Impact on N1530_ (Iterator Facade and Adaptor) ============================================== XXX