Concepts / Iterators and Container Traversal

Iterators and Container Traversal

A generic find_all() uses template parameters so that one definition works for strings, lists, and vectors, and it returns iterators so the caller can reach the matched positions.

  • Programming

Why One Search Must Serve Many Containers

Imagine you have written a routine that scans a container and collects the positions where a given value occurs. Once it works for a string of characters, the natural next step is to want the same routine for a list of doubles, a vector of strings, or any other sequence you might store. Rewriting the routine for every container type would duplicate the same loop again and again. This article follows one generic routine, find_all(), and shows how a single template can serve all of these containers, and what the template must say about the types involved in order to compile correctly.

What do you think happens?

When C is a template parameter, will the compiler always know on its own whether the name C::iterator refers to a type?

  • Yes, it can always infer the meaning from the name

  • No, the programmer has to mark it with typename when it is meant to be a type

Reveal answer
Answer:

No. The compiler cannot be relied on to infer that C::iterator is a type, so the programmer must mark it with typename.

In ordinary code a name usually has one obvious meaning. Inside a template, the name depends on what C turns out to be, and the compiler is not allowed to rely on guessing. The programmer has to say it.

Building the Generic find_all Routine

The routine find_all() takes a container and a value, walks through the container with an iterator, and returns a vector of the iterators that point at each matching element. Returning iterators rather than copies of the values is a deliberate design choice. The caller receives a handle to each match, so it can inspect or change the element in place.

Two template parameters do the work. C stands for whatever container type the caller passes in, and V stands for the type of the value being searched for. Because C is a parameter, the function accepts a string, a list, or a vector without any change to its body. The return type is a vector of iterators into that container, so the caller always receives positions expressed in the container's own iterator type.

cpp
Notes on the code
  • The template parameter C is the container type and V is the searched value type
  • The return type names a vector of the container's iterator type
  • The loop scans from begin() up to end(), comparing each element with v
  • Each matching iterator is collected and returned to the caller

The code above is an illustrative sketch that follows the description in the source: one signature, a scan from begin() to end(), a comparison of each element with the search value, and a vector of matching iterators as the result. Notice that the body never mentions string, list, or vector. That is the point of the template.

Receive containerand valueStart at begin()Element equals value?Store iterator inresultAdvance iterator++pAt end()?Return result vectoryesnonoyes
For any container type, the same steps run in the same order: start at begin(), compare each element with the search value, collect matching iterators, advance, and stop at end(). Only the element and iterator types change between calls.

Tracing find_all Across Container Types

The source tests the generic version on three containers: a string, a list of doubles, and a vector of strings. Each call uses the same template text, but the compiler produces a separate concrete version of find_all for each container type that is used. Each version has its own element type and its own iterator type, while the order of the steps stays the same.

CallContainer type CValue type VIterator type in the result
find_all on a stringstd::stringcharstring::iterator
find_all on a list of doubleslist<double>doublelist<double>::iterator
find_all on a vector of stringsvector<string>stringvector<string>::iterator

Three instantiations of one template. The loop text is identical; the types are not.

This is why the loop variable p in the test has a different concrete type in each call, even though the source text of the loop is the same in all three cases. The instantiation is decided by the container type the caller passes, and the caller never has to write three versions of the routine.

Where Matched Positions Come From

Because find_all() returns iterators rather than copies, a caller can write through them. The source extends the test with a loop over the results of find_all(vs, "green") that assigns a new string through each iterator. The matching elements of vs change in place, and the loop variable is only a handle to them. The diagram below uses a small container of our own, holding red, green, blue, and green, to show how each returned iterator maps to a specific position.

vs[0]redresult[0]iterator to position 1vs[1]greenresult[1]iterator to position 3vs[2]bluevs[3]greenpoints topoints to
After find_all(vs, green) returns, each iterator in the result points to one matching cell of vs. The two result iterators point at positions 1 and 3, so writing through them changes those two cells only.

The contents of vs in the diagram are our own illustration; the source describes the mechanism, not these particular values. The key idea is that each returned iterator is tied to a position in the container. If a result contains the wrong positions, the iterator is not the place to look first, because iterators faithfully record where the routine found each match. The error comes from the routine itself, specifically from the comparison that decides a match or the range that decides which elements are scanned.

The Dependent Name Problem

A dependent name is a name whose meaning depends on a template parameter. In C::iterator, the name iterator lives inside C, and C is only known when the template is used. Many learners assume the compiler will look at the name, notice that C is a template parameter, and work out on its own whether C::iterator is a type or some value. It is a reasonable guess, because in ordinary code a name usually has one obvious meaning. In a template, however, the compiler is not allowed to rely on guessing for it.

typenamemarks the name as a typeCtemplate parameter, knownonly at use::looks inside Citeratora type or a value?
The tokens of the declaration are read left to right. The typename token is the marker that tells the compiler the dependent name iterator, reached through C, is to be read as a type rather than a value.

The correction is that the programmer has to say it. The typename keyword is that marker. It tells the compiler that a dependent name refers to a type. Writing typename C::iterator is therefore a claim about meaning rather than decoration: it says the member named iterator inside C is a type to be used in a declaration. Without the marker, a declaration like the one below is not something the compiler can be trusted to read the way the author intended.

cpp
Notes on the code
  • The declared element type of the vector is an iterator type, so the name needs typename
  • The loop variable p is also declared with the dependent iterator type

Hiding the Iterator Type with an Alias

Repeating typename C::iterator in every signature and body is noisy, and it exposes an implementation detail that callers do not need to think about. The source introduces a type alias template, Iterator<T>, to hide it. A type alias gives a shorter name to a type expression. Here the alias takes a container type T and produces that container's iterator type, and the generic function uses the alias in place of the long form.

cpp
Notes on the code
  • The alias takes a container type T and names T::iterator once
  • The typename marker sits inside the alias, where the dependent name is written
  • The signature now reads at a higher level of intent
Before
template <class C, class V>
vector<typename C::iterator> find_all(C& c, V v);

Every signature repeats the typename keyword and the member name iterator

After
template <class C, class V>
vector<Iterator<C>> find_all(C& c, V v);

Every signature names Iterator<C>, and the knowledge of how to get the iterator lives in one place

The alias splits what each side needs to know. The caller passes a container and reads the result as a vector of iterators; the caller does not need to see the member name inside the container. The implementer writes the body and uses Iterator<C> wherever an iterator type is needed. The practical payoff is maintainability: if the way you obtain an iterator ever has to change, the change happens in the alias rather than in every function that uses it.

Worked Trace of a Green Match

Worked example

Tracing find_all(vs, green) and Locating a Wrong Result

A vector vs holds red, green, blue, and green. A call find_all(vs, green) is expected to return iterators to positions 1 and 3. Trace what the routine does and say where a wrong result would come from.

  1. 1
    Fix the types

    C is the vector of strings and V is the string type. The return type is vector<Iterator<C>>, which is a vector of vector<string>::iterator.

  2. 2
    Start the scan at begin()

    The loop variable p is set to the first element, the one holding red. It is a vector<string>::iterator.

  3. 3
    Compare each element with the value

    At position 0, red does not equal green, so nothing is collected. The scan advances with ++p.

  4. 4
    Collect the first match

    At position 1, the element is green and equals the search value, so p is stored in the result vector.

  5. 5
    Continue until end()

    Position 2 holds blue and is skipped. Position 3 holds green and is stored. The next advance reaches end(), and the loop stops.

  6. 6
    Return and inspect

    The result holds two iterators, pointing at positions 1 and 3. If the caller writes through them, only those two cells change.

Result

The result has two iterators, at positions 1 and 3. If a wrong set of positions appeared instead, check the comparison in the loop first, since it decides which elements are matches, and then the range from begin() to end(), since it decides which elements are scanned. A compile error on the return type, by contrast, points to a missing typename or a mistaken alias.

Mistakes With Dependent Names and Iterators

  • Common mistake

    Leaving out typename when a dependent name is used as a type

    What it looks like
    vector<C::iterator> result;
    Why it fails

    C::iterator depends on the template parameter C, and the compiler cannot be relied on to read it as a type without the marker.

    Do this instead
    vector<typename C::iterator> result;
  • Common mistake

    Assuming the compiler will infer the meaning from the name

    Why it fails

    The name's meaning depends on what C turns out to be, so the compiler is not allowed to settle the question by guessing.

    Do this instead

    Mark every dependent name that is meant to be a type with typename.

  • Common mistake

    Repeating typename C::iterator in every signature and body

    Why it fails

    The repetition exposes an implementation detail that callers do not need, and it spreads the knowledge of how to get an iterator across the code.

    Do this instead

    Record the detail once in a type alias such as Iterator<T> and use Iterator<C> in the signatures.

  • Common mistake

    Treating a wrong set of positions as a type error

    Why it fails

    A wrong set of positions usually comes from the comparison or the scan range, not from the signature.

    Do this instead

    Check the comparison between element and value, and the range from begin() to end(), before changing the return type.

  • Common mistake

    Expecting the result to be copies of the elements

    Why it fails

    find_all returns iterators, so writing through a result changes the container itself.

    Do this instead

    Treat each result element as a handle to a position in the original container.

Practice Tracing a Search

EASY

Given the generic template template <class C, class V> vector<typename C::iterator> find_all(C& c, V v), and a call find_all(s, x) where s is a std::string, explain what the element type of the returned vector is. Then say where you would look first if the returned positions were wrong, and where you would look first if the code failed to compile on the return type.

Hints
  • The return type is a vector of the container's own iterator type, and C is the string type here
  • A wrong set of positions points to the comparison or the scan range
  • A compile error on the return type points to a missing typename or a mistaken alias

Wrapping Up the Traversal Pattern

  1. 1
    A generic find_all() uses template parameters so one definition serves strings, lists, and vectors, and it returns iterators so the caller can reach each matched position.
  2. 2
    A dependent name such as C::iterator must be marked with typename when it is meant to be a type, because the compiler cannot be relied on to infer that meaning.
  3. 3
    The alias template Iterator<T> hides the typename C::iterator detail, keeping signatures shorter and easier to maintain.
  4. 4
    Each container type produces its own concrete version of the function, so the same loop yields string, list, or vector iterators.
  5. 5
    When a generic search goes wrong, check the comparison and the begin-to-end range for wrong positions, and the typename marker or alias for compile errors on the return type.

Key Takeaways

  • A generic find_all() uses template parameters so one definition serves strings, lists, and vectors, and it returns iterators so the caller can reach each matched position.
  • A dependent name such as C::iterator must be marked with typename when it is meant to be a type, because the compiler cannot be relied on to infer that meaning.
  • The alias template Iterator<T> hides the typename C::iterator detail, keeping signatures shorter and easier to maintain.
  • Each container type produces its own concrete version of the function, so the same loop yields string, list, or vector iterators.
  • When a generic search goes wrong, check the comparison and the begin-to-end range for wrong positions, and the typename marker or alias for compile errors on the return type.

Related concepts

More concepts starting with I