Concepts / Iterator Traits and Type Functions

Iterator Traits and Type Functions

The standard header <iterator> provides the class template iterator_traits, which supplies information about iterators.

  • Programming

Two Lines That Name Types

Most code you write works with values: numbers, strings, and containers that hold them. Some code has to work with types instead, and it has to make that choice while the program is being compiled, before any values exist. This article starts from two short lines of C++ that do exactly that kind of work, and then shows how the standard library applies the same technique to iterators.

Keep one question in mind as you read: if a generic function needs to behave differently depending on what kind of iterator it receives, how can it find out what kind that is without running any code first? The answer the standard library uses is the subject of the rest of this article. The two names that the source introduces, Iterator_type and Iterator_category, each stand for a type rather than a value, and tracing what each one refers to is the best way to see the whole mechanism.

What a Type Function Does

Definition

A type function takes a type as input and produces a type as output, and it does this at compile time. No values are computed while the program runs; the compiler works out the answer while it translates the code.

The Iterator_type alias from the source is a type function. Given a container type C, it names C's iterator type. You can think of it as a lookup table with one row: put in a container type, and the alias gives back the iterator type that container declares. Because the lookup happens in the compiler, the result can shape the code that is generated for that exact type.

A class template is the pattern the compiler uses to build a concrete class for a specific type. Writing std::iterator_traits<Iter> with a concrete iterator type in place of Iter asks the library for the information it holds about that iterator. The source describes this information as a set of type-level facts, and the one it names is the iterator category, accessed as the member iterator_category.

Reading Iterator_type Step by Step

The pattern from the source can be written in a generic form. The first line declares a template parameter C that stands for any container type. The second line defines a new name, Iterator_type, as an alias for C::iterator. The diagram below labels each part of that line so you can see what each piece contributes.

cpp
Notes on the code
  • template <typename C> declares C as a placeholder that the compiler fills in with a real type later
  • using Iterator_type = ... defines a template alias, a new name for a type computed from C
  • typename tells the compiler that C::iterator names a type
  • C::iterator is the nested member path that is looked up inside whatever type C turns out to be
templateintroduces a pattern overtypestypename Cplaceholder for anycontainer typeusing Iterator_typethe alias name beingdefinedtypenamemarks the next name as atypeC::iteratornested member path lookedup in Cdeclaresused bydefined asqualifies
What does each token of the alias line contribute to the type function?

Read the alias from left to right in the same order the compiler does. The template line says the alias works for any C. The using line gives the new name. The typename keyword marks C::iterator as a type. The final part, iterator, is a member that C must provide. If you substitute a concrete container type for C, the compiler looks inside that container for its member named iterator and returns the type it finds there.

Why typename Appears in the Alias

The typename keyword appears in the middle of the Iterator_type line for a reason the source spells out. Because the expression depends on the template parameter C, the compiler cannot know on its own whether C::iterator names a type or something else, such as a value or a function. Writing typename tells it that the name is a type. The same keyword appears in the Iterator_category line for the same reason, where iterator_traits<Iter> depends on the template parameter Iter and the compiler needs to be told that iterator_category is a type.

What iterator_traits Returns for an Iterator

The standard header <iterator> provides the class template iterator_traits, which supplies information about iterators. The information comes out as member names. The member named in the source is iterator_category, and you read it by writing the name of the iterator type inside the angle brackets and then the member name after the scope operator. The diagram shows the lookup path: the iterator type goes into the template, and the member comes out.

iterator_traits<It>class template instantiatedfor ItIt with nestedmembersiterator type that declaresits own member typesiterator_categorythe category type thelibrary exposesraw pointerplain pointer with nonested membersmembers read throughmapped to the same set of typesprovides member
How does iterator_traits<It> reach the member types of a given iterator type, and how does a raw pointer with no nested members map to the same set of types?

The diagram also shows something the source does not spell out in detail: a raw pointer does not declare nested members of its own, yet iterator_traits still gives it the same set of type-level facts. This is why generic code can ask for iterator_category without first checking whether the iterator is a class or a pointer. The lookup is uniform across both.

Tracing a Type Function Through a Container

The example below is an original illustration that uses only the pattern shown in the source. It defines a small container type whose iterator member is a plain pointer. Follow the alias from the container type to the answer it produces.

cpp
Notes on the code
  • Int_box declares a nested name iterator that is an alias for int*
  • Iterator_type<Int_box> substitutes Int_box for C
  • The compiler resolves Int_box::iterator, which is int*
  • p is therefore declared as an int* that starts out null
Worked example

Resolving Iterator_type<Int_box>

What type does Iterator_type<Int_box> name?

  1. 1
    Start with the alias

    Iterator_type is defined as typename C::iterator for any C.

  2. 2
    Substitute the container

    Replace C with Int_box, giving typename Int_box::iterator.

  3. 3
    Look up the nested member

    Int_box declares iterator as int*, so the lookup finds that member.

  4. 4
    Read the result

    The alias names int*, and the declaration of p uses that type.

Result

Iterator_type<Int_box> names int*.

Tag Dispatch Through Iterator Categories

Tag dispatch is a technique that uses iterator categories to choose among different implementations of the same work. One generic function can pick one of several versions based on the kind of iterator it receives. The category of the iterator acts as a tag, and the compiler selects the matching implementation at compile time. The source states that tag dispatch is supported by iterator_traits, which is why the category is read through iterator_traits rather than computed by hand.

sort(first, last)generic call on an iteratorrangeiterator_traits<It>::iterator_categorycategory read from thetraitscategory tag argumentcategory passed as a tagvalueoverload selectioncompiler picks the matchingoverloadchosen implementationversion of the work forthat categoryasks for categorypassed ascompared against overloadscompiler selects
In what order do the algorithm call, the category tag argument, the overload selection, and the chosen implementation happen when sort() runs on a given iterator type?

Read the diagram from top to bottom. The generic code never checks the iterator's kind by hand. It asks iterator_traits for the category, and the category decides which implementation the compiler uses. This is the connection between the two ideas in this article: iterator_traits supplies the type-level information, and tag dispatch uses that information to choose behavior.

Why sort() Depends on Type Functions

The source says that iterator_traits gives simple definitions of the type functions used in sort() and supports tag dispatch. In other words, sort() does not hard-code what kind of iterator it is working with. It reads the category through a type function and lets tag dispatch pick the version of the work that suits that category.

The plan for this article notes that sort() only compiles when the iterator category supports constant-time jumps, which is the random-access category. The reasoning follows from the dispatch step above: if the category does not offer that capability, the overload chosen for sort() cannot be built for that iterator type. Use the category tag, not your own guess about the iterator, to decide whether a given iterator can be sorted.

Mistakes in Type-Level Lookups

  • Common mistake

    Omitting typename before a dependent nested name

    What it looks like
    template <typename C>
    using Iterator_type = C::iterator;
    Why it fails

    C depends on the template parameter, so the compiler cannot tell whether C::iterator is a type. Without typename it does not treat the name as a type, and the alias fails to resolve.

    Do this instead
    template <typename C>
    using Iterator_type = typename C::iterator;
  • Common mistake

    Naming a member the container does not declare

    What it looks like
    struct Int_box {
        using iterator = int*;
    };
    Iterator_type<Int_box>::value_type x;
    Why it fails

    The lookup inside Int_box finds only the members it declares. A name it does not provide cannot be resolved, so the failing name is the one that is missing from the container.

    Do this instead

    Check that the container declares the member the alias asks for, and name only that member.

  • Common mistake

    Assuming a raw pointer has no type-level information

    Why it fails

    iterator_traits supplies the same set of type-level facts for a raw pointer as it does for a class iterator, so generic code does not need to special-case pointers.

    Do this instead

    Read the category through iterator_traits<It> for every iterator type, including pointers.

Edge case

When a type lookup fails, the step to inspect is the nested name lookup, not the outer alias. Find the qualified name that the compiler could not resolve as a type, which is the name after the scope operator in the lookup path, and check whether typename appears in front of it and whether the container actually declares it.

Practice: Find the Failing Name

MEDIUM

Consider this alias: template <typename C> using Iterator_category = std::iterator_traits<C>::iterator_category; Explain at which step the lookup could fail, and name the identifier that the compiler would report as the failing one if the keyword typename were removed.

Hints
  • Ask whether the expression std::iterator_traits<C> depends on the template parameter C.
  • Identify the last name in the path, the member after the scope operator, and decide whether it is meant to be a type.
  • The source says that typename tells the compiler that the name is a type when the expression depends on a template parameter.

Key Takeaways

  • A type function works on types at compile time, and a template alias such as Iterator_type = typename C::iterator names the type it produces.
  • The class template iterator_traits, provided by the header <iterator>, supplies type-level information about an iterator, including the member iterator_category.
  • The typename keyword is required when a name depends on a template parameter, because it tells the compiler that the name is a type.
  • Tag dispatch uses the iterator category, read through iterator_traits, to select among implementations, and sort() relies on this mechanism.

Related concepts

More concepts starting with I