Iterator Categories and Their Capabilities
Tag dispatch selects between algorithms based on the type of iterator provided, and the selection happens at compile time.
One Algorithm Name, Two Possible Behaviors
Generic algorithms receive iterators, and iterators do not all offer the same abilities. Some iterators let you move to any position in a sequence directly, while others can only advance one element at a time. An algorithm written to take advantage of direct movement may not suit an iterator that can only move forward, and the reverse is also true. Programmers therefore want a way for one algorithm name to route the work to the version that fits the iterator in hand.
The technique that handles this routing is called tag dispatch. Its defining feature is that the choice between alternatives is made based on the type of iterator that was provided, and it is made at compile time. This article follows the path a single iterator takes through that routing, then looks at the mistakes readers most often make along the way.
What do you think happens?
When the program runs, does it inspect the iterator to decide which helper to use?
Reveal answer
No, the choice is made at compile time.
The comparison between tag values and helper arguments is resolved while the program is being compiled, so the program that runs already contains the selected helper. Nothing has to inspect the iterator during execution to decide which algorithm to use.
Recovering the Iterator Type from a Container
The first step in the path is to recover the iterator type from the container type alone. A generic function that has been handed a container type can use Iterator_type<C> to name the iterator type that belongs to that container. The source describes this as returning C::iterator, so the function does not need to know anything more about the container in advance.
The result of this step is still a type, not a value. Nothing has been asked to move or to inspect the container yet. The step only answers the question of which iterator type belongs to this container.
Turning a Type into a Tag Value
A type alone cannot be passed into a function in the way a value can, so the next step converts the iterator type into a value. Iterator_category<Iter>{} does this. It constructs a tag value that indicates the kind of iterator Iter is. The tag is what the helper overloads are matched against.
It helps to read the expression from left to right as a small sentence. The function name says that a category is being determined. The angle brackets say which iterator type the category is being determined for. The empty braces say that a value of the resulting kind is being produced. The output is a tag whose job is to carry information about the iterator's kind into a function call, where overload selection can use it.
Two Helpers Sharing One Name
The technique is built from two helper functions. They share a name. Each one takes an extra argument whose only purpose is to say which kind of iterator that helper is meant for. One helper is intended for random-access iterators, and the other is intended for forward iterators.
Because the extra argument carries the tag, the two helpers can share a name and still be told apart when a call is made. The name is the same for both, so the caller writes one algorithm name, and the tag argument decides which helper body is the match.
The random-access helper is the more straightforward of the two. Its body is trivial, which means there is little work to do once the right helper has been chosen. The interesting part of the design lies in the selection, not in the bodies. That is why most of the explanation concerns how the right helper gets picked.
Tracing a Call Through the Branch
The diagram below shows the order of events from the call to an algorithm to the overload that actually gets compiled. The branch sits at the point where the tag is compared against the helpers' extra arguments. That comparison is resolved while the program is compiled, so the branch does not exist as a decision at run time.
Following one iterator through the routing
An algorithm is called with an iterator whose kind is random-access. Trace which helper is selected and when that selection happens.
- 1Step 1: Recover the iterator type
Iterator_type<C> gives the iterator type that belongs to the container. This is still only a type, and nothing has moved or been inspected.
- 2Step 2: Build the tag
Iterator_category<Iter>{} creates a tag object that indicates the iterator is random-access. The braces make it a value that can be passed into a call.
- 3Step 3: Match the tag to a helper
The tag is passed alongside the algorithm's arguments. The helper whose extra argument is meant for random-access iterators is the match, and the other helper is not chosen.
- 4Step 4: Confirm when this happened
The match is resolved while the program is compiled. The compiled program already contains the random-access helper, so no check of the iterator happens while the program runs.
The random-access helper is selected at compile time, based on the iterator type the algorithm was given. Only that helper is part of the path the program takes.
Common Misreadings of Tag Dispatch
Common mistake Thinking Iterator_category<Iter>{} is a type
What it looks likeReading the expression as a name for a kind of iterator, the way Iterator_type<C> names an iterator type.
Why it failsThe braces create an object. The output is a tag value, and a value can be handed to a function call, which a bare type name cannot.
Do this insteadRead the trailing braces as the step that turns the type into a value carrying the iterator's kind.
Common mistake Believing Iterator_type<C> moves or inspects the container
What it looks likeAssuming that calling Iterator_type<C> starts traversal or looks at the container's contents.
Why it failsIterator_type<C> only recovers the iterator type that belongs to the container. Nothing has been asked to move or to inspect the container yet.
Do this insteadTreat Iterator_type<C> as a type-level lookup that happens before any iteration.
Common mistake Thinking the choice of helper is made at run time
What it looks likeImagining the program checking the iterator's kind each time the algorithm runs and then branching.
Why it failsTag dispatch is defined by its selection at compile time. The compiled program already contains the selected helper, so there is no run-time inspection to decide which algorithm to use.
Do this insteadLocate the branch at compile time, where the tag is compared against the helpers' extra arguments.
Common mistake Assuming the helper bodies hold the interesting logic
What it looks likeLooking for the important design decisions inside the random-access helper's body and expecting them to explain the routing.
Why it failsThe random-access helper's body is trivial. The interesting part of the design is the selection that routes the call to the right helper.
Do this insteadFocus on how the tag and the extra argument pick the helper, rather than on what each helper does once chosen.
Common mistake Believing the two helpers have different names
What it looks likeExpecting a separate algorithm name for random-access and forward iterators.
Why it failsThe two helpers share a name. The extra argument is what lets them be told apart when a call is made.
Do this insteadRemember that one shared name plus a tag argument is what lets one algorithm name serve several iterator kinds.
Tracing a Dispatch Yourself
Without looking back, write the four stages a single iterator passes through in tag dispatch, in order, and for each stage say whether it produces a type or a value. Then say at which stage the helper is chosen and whether that choice happens before or during program execution.
Hints
- The first stage produces a type and the second produces a value.
- The stage that carries the iterator's kind into a call is the tag.
- Ask yourself whether the comparison between tag and extra argument is something the running program has to perform.
What the Category Choice Changes
Tag dispatch lets one algorithm name serve iterators with different abilities. The routing is built from a type lookup, a value construction, and an overload match, and the match is made at compile time. The standard library relies on this technique, which is why it is used to improve both flexibility and performance.
Key Takeaways
- Algorithms may need different behavior depending on whether an iterator can jump directly to a position or can only advance one element at a time.
- Iterator_type<C> recovers the iterator type from a container type, and this step produces a type rather than a value.
- Iterator_category<Iter>{} uses its braces to create a tag value that carries the iterator's kind into a function call.
- Two helpers share one name and are told apart by an extra argument, one for random-access iterators and one for forward iterators.
- The helper is selected at compile time, so the compiled program already contains it and no run-time check of the iterator is needed.