Concepts / Type Aliases in C++

Type Aliases in C++

The return type is a vector of iterators into that container.

  • Programming

One Search Routine for Many Containers

Suppose 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, you will quickly want the same routine for a list of doubles, a vector of strings, or any other sequence. Rewriting the loop for every container type would duplicate the same logic again and again. The goal of this article is to see 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.

The routine in question is find_all(). It 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 an important design choice. The caller receives a handle to each match, so it can inspect or change the element in place.

Two Template Parameters at Work

Two template parameters do the work in the generic version. 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. The caller therefore always receives positions expressed in the container's own iterator type. The iterator type is not a fixed type that every container shares. It is a member that each container defines for itself, and that is where the difficulty begins.

cpp
Notes on the code
  • C and V are the two template parameters: the container type and the searched-value type
  • The return type names the container's iterator type inside a vector
  • The loop variable p is declared with the same nested iterator type
  • The comparison and push_back collect positions, not copies of the matched values

The Guess the Compiler Cannot Safely Make

Before reading the generic version, pause on a belief many learners bring to templates. The belief is that the compiler will look at a name such as C::iterator, notice that C is a template parameter, and figure out on its own whether the name refers to a type or to some value. It is a reasonable guess, because in ordinary code a name usually has one obvious meaning. In a template, however, the name depends on what C turns out to be, and the compiler is not allowed to rely on guessing for it.

What do you think happens?

Inside a template, if C::iterator is written without any marker, can the compiler be relied on to treat it as a type?

  • Yes, the compiler always infers that a nested name like this is a type

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

Reveal answer
Answer:

No, the programmer must mark the name with typename when it is meant to be a type

A dependent name is one whose meaning depends on a template parameter. The compiler may read such a name as a value unless the programmer marks it as a type, and the typename keyword is that marker.

Read C::iteratorinside a templateDoes C depend on atemplate parameter?C is a parameterDependent namemeaning deferredIs typename writtenbefore it?marker checkRead as a typeintended for declarationsMay be read as avaluecompiler cannot be reliedonyesyesno
When the compiler reads C::iterator inside a template, the presence or absence of typename decides how the name is read.

The correction is that the programmer has to say it. Without the typename marker, a declaration like vector<typename C::iterator> is not something the compiler can be trusted to read the way the author intended. The flowchart above shows the decision point: the marker is what turns a dependent name into one read as a type.

What the typename Keyword Declares

The typename keyword tells the compiler that a dependent name refers to a type. The source puts the reason plainly: C's iterator is supposed to be a type, not a value of some type such as the integer 7. Writing typename C::iterator is therefore a claim about meaning, not a decoration. It says that the member named iterator inside C is a type to be used in a declaration.

typenamesays the next name is atypeCtemplate parameter, chosenby caller::look inside C for a memberiteratornested name defined by thecontainer
Each token in the declaration contributes a different part of the meaning: the marker, the template parameter, the scope operator, and the nested name.

Hiding the Iterator Behind 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 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 Iterator is a template that takes a container type T
  • typename appears once, inside the alias, where the dependent name is defined
  • The signature and loop now read Iterator<C> instead of the long nested form

The difference between the two approaches is one of where the detail lives. With the bare form, every function that needs an iterator must repeat the typename and the member name. With the alias, that knowledge is recorded once, and the function signatures state their intent at a higher level. The source notes that this improves readability and maintainability, which is the practical payoff: if the way you obtain an iterator ever has to change, the change happens in one place.

AspectBare form: typename C::iteratorAlias form: Iterator<C>
Where the typename detail livesRepeated in every signature and bodyRecorded once, inside the alias
What a caller readsThe implementation detail of the containerA shorter name for the container's iterator
Effect of changing how an iterator is obtainedEdit every place it is writtenEdit the alias definition

Two ways to name the same iterator type

Tracing One Call Across Containers

The same loop text runs whether the container holds characters, doubles, or strings, because the template generates a separate version of the function for each container type that is used. What changes from one call to the next is the type of the elements being compared and the type of the iterators being collected, not the order of the steps. The pipeline below follows one call through its decision points.

Call find_all(c, v)C and V are deduced fromargumentsIterator type fromsignatureIterator<C> names thecontainer's iteratorScan begin() to end()range of positions visitedCompare element tovaluecomparison decides a matchReturn vector ofiteratorshandles to each match
The steps are the same for every container; the iterator type and element type change with the container passed in.
Worked example

Locating where a wrong result begins

A call to find_all() returns positions that look wrong for one of three containers: a std::string, a std::list of doubles, and a std::vector of strings. Which of the three decision points should you check first, and why?

  1. 1
    Check the signature

    Confirm the iterator type the signature promises. A compile error on the return type usually points at a missing typename or a mistaken alias, so a compile error here is a different symptom from a wrong set of positions.

  2. 2
    Check the scan range

    Confirm that the loop runs from begin() to end() for the container in question. A wrong set of positions can come from a range that does not cover what you expected.

  3. 3
    Check the comparison

    Confirm that the element compared with the search value is the one you think it is. A wrong set of positions usually comes from the comparison or the range.

  4. 4
    Trace one container at a time

    Run the trace separately for the string, the list, and the vector. The container where the result diverges shows which of the three points changed.

Result

The unexpected result is traced to the decision point where the behavior for that single container diverges, rather than being guessed at across all three.

Returned Iterators Let the Caller Write

Because the function 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. This is the reason the result is a vector of iterators: the caller gets access to the container itself.

An iterator returned by find_all() acts as a position inside the container. The caller can move from that position to neighboring elements by stepping the iterator, which is why the return type names the container's own iterator type rather than a plain value.

Mistakes When Reading Generic Search Code

  • Common mistake

    Assuming the compiler will decide whether a dependent name is a type

    What it looks like

    Writing vector<C::iterator> inside a template and expecting it to be read as a type.

    Why it fails

    A dependent name's meaning depends on the template parameter, and the compiler may read it as a value unless the programmer marks it.

    Do this instead

    Write vector<typename C::iterator> when the name is meant to be a type.

  • Common mistake

    Dropping typename from the alias definition

    What it looks like
    template <typename T> using Iterator = T::iterator;
    Why it fails

    The alias is still a dependent name, so the type claim must be marked explicitly.

    Do this instead
    Write template <typename T> using Iterator = typename T::iterator;
  • Common mistake

    Treating the returned iterators as copies of the elements

    What it looks like

    Assuming that changing the loop variable in the caller leaves the container unchanged.

    Why it fails

    The function returns iterators, which are handles into the container, so writing through them changes the matched elements.

    Do this instead

    Remember that each iterator points at a position in the original container.

  • Common mistake

    Blaming the comparison when the compile error is about the return type

Checking a Trace on Your Own

MEDIUM

Take the generic find_all() signature with Iterator<C> as the return type. Suppose a caller passes a std::vector of strings and the function returns an unexpected set of positions, while the return type compiles without error. Which of the three decision points would you check first, and what evidence would tell you the cause is in that point?

Hints
  • A clean compile suggests the signature and alias are not the first place to look.
  • Compare the elements that were matched against the value that was searched for.
  • Ask whether the scan covered the full range from begin() to end().

Key Takeaways

  • A generic find_all() uses template parameters, usually C for the container and V for the searched value, so one definition works for strings, lists, and vectors.
  • 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 find_all, and the returned iterators let the caller reach and change matched elements in place.
  • When a generic search goes wrong, check the iterator type the signature promises, the scan range, and the comparison, one container at a time.

Related concepts

More concepts starting with T