Concepts / Iterator Categories and Their Capabilities

Iterator Categories and Their Capabilities

Tag dispatch selects between algorithms based on the type of iterator provided, and the selection happens at compile time.

  • Programming

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?

  • Yes, the program inspects the iterator at run time

  • No, the choice is made at compile time

Reveal answer
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.

Container type Ca type, not a valueIterator_type<C>returns C::iteratorIterator type Iterstill a typeIterator_category<Iter>{}braces create an objectTag valuecarries the iterator kindMatching helperoverloadchosen by the tagpassed innames the typeused as argumentconstructspassed to a call
How does a container type flow through Iterator_type to get an iterator type, then through Iterator_category to become a tag object that is passed to an overload?

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.

Algorithm calledreceives an iteratorIterator_category<Iter>{}builds the tagWhich tag is it?resolved at compile timeRandom-access helperextra argument marksrandom-accessSelected helper inprogramalready chosen beforerunningForward helperextra argument marksforwarditerator typetag passed alongrandom-access tagforward tagcompiled incompiled in
What steps happen, in order, from the call to an algorithm to the overload that actually gets compiled, and where does the branch happen?
Worked example

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.

  1. 1
    Step 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.

  2. 2
    Step 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.

  3. 3
    Step 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.

  4. 4
    Step 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.

Result

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 like

    Reading the expression as a name for a kind of iterator, the way Iterator_type<C> names an iterator type.

    Why it fails

    The 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 instead

    Read 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 like

    Assuming that calling Iterator_type<C> starts traversal or looks at the container's contents.

    Why it fails

    Iterator_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 instead

    Treat 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 like

    Imagining the program checking the iterator's kind each time the algorithm runs and then branching.

    Why it fails

    Tag 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 instead

    Locate 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 like

    Looking for the important design decisions inside the random-access helper's body and expecting them to explain the routing.

    Why it fails

    The 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 instead

    Focus 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 like

    Expecting a separate algorithm name for random-access and forward iterators.

    Why it fails

    The two helpers share a name. The extra argument is what lets them be told apart when a call is made.

    Do this instead

    Remember that one shared name plus a tag argument is what lets one algorithm name serve several iterator kinds.

Tracing a Dispatch Yourself

MEDIUM

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.

Related concepts

More concepts starting with I