Iterator Categories and Their Operations
The range sort() requires random-access iterators, a capability that depends on the iterator a container provides.
A Sort That Asks More of Its Input
Suppose you have a collection of values and you want them in order. A natural design is a sort function that receives the container itself and does all the work. The standard library's range version of sort() is shaped differently. It does not take a container. It takes a pair of iterators that together define a sequence: one marks where the sequence begins, the other marks where it ends, and sort() works on everything between them.
That shape looks harmless, but it carries a hidden requirement. The iterators passed to sort() are not allowed to be just any iterators. They must offer random access to the sequence. Whether a given call is valid therefore depends on what kind of iterator the container provides, not only on whether the container can be looped over. This article follows that requirement from the call site down into the iterator itself.
What do you think happens?
Can a forward_list, which you can walk from front to back with a loop, be passed to the range version of sort()?
Reveal answer
No, because its iterators do not offer random access
A forward_list offers forward iterators, which let a program move from front to back. Traversal alone is not enough for sort(), which needs random access. The rest of this article explains why the requirement is on the iterator rather than on the container's name or size.
Tracing Positions in Two Layouts
The reason random access matters becomes clearer when you picture how the two kinds of container store their elements. In a random-access container, the elements sit at numbered positions, and a program can jump straight to position i. In a forward_list, each element holds a link to the element after it and nothing else useful for navigation. To reach position i, you must start at the beginning and follow links one at a time.
Read the diagram as a contrast in access, not a ranking of quality. The top row can be reached by naming a position. The bottom row can only be reached by walking. The links in the forward_list also point only forward, because each element knows about the element after it and not about the one before it. That is why stepping back is not a reasonable operation for this container.
Random Access and the Precondition on sort()
sort() moves elements around in a non-sequential way. It does not simply pass once from front to back, swapping neighbours as it goes. Because it reorders elements across the whole sequence, it needs to reach any position directly. Random access is the capability that provides this: it lets a program reach any position in the sequence without stepping through each element before it.
A precondition is a requirement that must hold before a function can be called validly. For the range version of sort(), the precondition is that the iterators passed in must offer random access to the sequence they delimit.
Random access is therefore a property of how a container's iterators behave. A container can hold a great many elements, be walked from start to finish, and still fail this precondition if its iterators cannot jump to arbitrary positions.
How iterator_traits Lets One sort() Serve Many Containers
If a sort function simply accepted any pair of iterators, it would fail on a forward_list at the point where it tried to use a capability that iterator does not have. A better design checks the kind of iterator before it commits to a strategy. The standard library provides a mechanism named iterator_traits for exactly this purpose. It allows a program to check which kind of iterator a container supports.
The source uses this mechanism to improve the range sort() function from an earlier section, so that it accepts either a vector or a forward_list. The goal is a single function that can be called on both containers, with the kind of iterator determining what the function is able to do. The example is written in C++, the language the source uses for the standard library. Its value here is the calling pattern: one function name, two different containers, and no separate sorting function for each.
The diagram shows the order of decisions. The category is looked up before the sorting work begins, so the function's behaviour follows from the kind of iterator it was given. The same function name therefore covers both containers, while the iterator determines which path is open to it.
Locating a Mismatch at the Call
Consider a call such as sort(lst), where lst is a forward_list. When you read this call, the question to ask is not whether the container holds numbers or can be walked from front to back. The question is whether its iterators meet the requirement of the function being called. The sequence below follows that question through the call.
Tracing sort(lst) on a forward_list
A forward_list named lst holds the values 3, 1, 2. Trace what happens when the range version of sort() is called with its begin and end iterators.
- 1Step 1: the call
The caller passes the iterators that mark the beginning and end of lst. The container's values, 3, 1, 2, are not examined yet.
- 2Step 2: the requirement
sort()needs random access to the sequence, because it reorders elements across the whole range rather than walking once. - 3Step 3: the category check
The iterators are the forward kind, which can move from front to back but cannot jump to an arbitrary position. The requirement is not met.
- 4Step 4: where the error belongs
The problem is located at the
sort(lst)call, in the iterator's kind. The values 3, 1, 2 and any loop around the call are not the cause.
The call is rejected because the forward_list iterators do not meet sort()'s random-access requirement, even though lst can be traversed from front to back.
Notice that the same values, the same loop structure, and the same container could be fine for a different algorithm that only needs forward traversal. The mismatch is between the container's iterator kind and the function's requirement, which is why it is found at the call and not inside the element values.
Mistakes Beginners Make with Iterator Requirements
Common mistake Assuming that a container you can loop over can always be sorted with the range version of sort()
What it looks likeA forward_list can be walked from front to back with a for-statement, so it seems it should work with sort(lst).
Why it failsTraversal and random access are different capabilities. The range
sort()requires random access, and aforward_listonly offers forward iterators.Do this insteadCheck the iterator kind the function requires, not just whether the container can be traversed.
Common mistake Deciding whether a container supports sort() from its name or its size
What it looks likeTwo containers of the same size are assumed to behave the same way with sort().
Why it failsThe source places the precondition on the iterator, not on the container's name or size.
Do this insteadAsk what kind of iterator the container provides and whether it offers random access.
Common mistake Looking for the cause of the failure in the element values or the surrounding loop
Why it failsA mismatch between container and iterator kind is located at the call, in the requirement.
Do this insteadStart debugging at the sort() call and compare the iterators passed with the requirement the function states.
Practice Predicting Which Containers Fit
A colleague says that because a forward_list can be walked with a for-statement, the range version of sort() must accept it too. Explain in your own words why this reasoning is incomplete, and name the capability the iterator would need to provide.
Hints
- Separate traversal from random access in your explanation.
- Think about what
sort()must do to elements across the whole range. - Remember that the source places the precondition on the iterator, not the container's name or size.
Given a call sort(lst) where lst is a forward_list, state the exact point in the call where the mismatch would be identified, and say which part of the program that point belongs to.
Hints
- The mismatch is located at the call, in the requirement.
- It is not in the element values or in the loop around the call.
Key Takeaways on Iterator Kinds
- 1The range version of
sort()takes a pair of iterators and requires that they offer random access to the sequence. - 2A
forward_listis singly-linked, so it offers forward iterators that traverse front to back but not random access. - 3
iterator_traitslets a program check which kind of iterator a container supports, so onesort()function can accept both a vector and aforward_list. - 4A mismatch between container and iterator kind surfaces at the
sort()call, in the requirement, not in the element values or the surrounding loop.
At a glance
- The range version of
sort()requires random-access iterators because it reorders elements across the whole sequence. - A
forward_listoffers forward iterators, which can traverse front to back, but its singly-linked structure gives no direct jump to arbitrary positions. iterator_traitslets a program check the iterator kind, which allows onesort()function to serve both a vector and aforward_list.- The precondition belongs to the iterator, so a mismatch is located at the
sort()call in the requirement, not in the element values or loop.