Concepts / Standard Library Containers

Standard Library Containers

Because the container choice and the element choice are independent, sum() is generic in two dimensions.

  • Programming

Why One Routine Should Serve Many Types

Suppose you have written a routine that adds up the values in a collection. Today the values are whole numbers, so you write it for int. Next week you need the same total for decimal measurements, and after that for a grid of numbers that represents a matrix. Copying and editing the routine each time works, but it duplicates logic and invites mistakes. Generic programming asks a different question: can one algorithm be written once and still be correct for many kinds of data?

What do you think happens?

Before reading on: if you need the same total for whole numbers, decimals, and matrices, what is the cost of copying and editing the routine each time?

  • Nothing; copying is always the cleanest approach

  • It duplicates logic and invites mistakes, even though it produces working code

  • It makes the program run faster

Reveal answer
Answer:

It duplicates logic and invites mistakes, even though it produces working code

The source says copying and editing the routine works, but it duplicates logic and invites mistakes. Generic programming aims to write the algorithm once and keep it correct for many kinds of data.

This article follows one thread from that question. First you will see what makes an algorithm generic, then how C++ templates support it, then what a container is, and finally how range checking protects you from asking a container for something it does not hold. Each stage builds on the one before it, so keep the question of valid inputs in mind throughout.

Requirements, Not Lists of Allowed Types

Definition

Generic programming designs general algorithms that accept a wide variety of types, provided those types meet the algorithm's requirements on its arguments.

The word that carries the most weight in that definition is requirements. Generic does not mean that every type is automatically acceptable. It means the algorithm is written in terms of what it needs from its inputs rather than in terms of one specific type. If a type supplies what the algorithm needs, the same algorithm body can be used with that type. If it does not, the algorithm's requirements are unmet, and the type is not a valid argument.

Templates and Compile-Time Parametric Polymorphism

In C++, the template is the main language feature that supports generic programming. A template lets you write an algorithm with a type that is not yet fixed. The algorithm is then used with particular concrete types. The source describes this as compile-time parametric polymorphism: one piece of code is written in a parameterized form, and the parameter, which is a type, is supplied when the code is used.

Definition

Compile-time parametric polymorphism means one algorithm is written with a type parameter, and the concrete type is chosen during compilation rather than while the program runs.

The adjective compile-time tells you when this happens. The choice of type is part of what the compiler works with, not something decided while the program runs. Because of this, one algorithm definition can serve many valid container and element combinations, and each combination that is used is prepared for that particular type when the code is compiled.

Generic algorithmdefined onceintvalid when requirements aremetdoublevalid when requirements aremetMatrixvalid when requirements aremetapplied toapplied toapplied to
Which element types can the same generic algorithm be used with, and does the algorithm itself change between those uses?

The diagram shows the relationship in words: the algorithm is defined once, and each element type that meets its requirement can be used with it. Notice that the algorithm does not change between these uses. Only the type it is applied to changes.

Tracing sum() Across Two Dimensions

The source uses sum() as its main illustration. The sum() algorithm is generic in two dimensions at once, and each dimension can vary independently while the algorithm itself stays the same. The worked example below follows the reasoning for each dimension.

Worked example

Checking whether sum() accepts a new combination

Could sum() be used on a list of double values, given that a list supports begin() and end() and double can be added to the Value argument?

  1. 1
    Check the container requirement

    sum() works on any data structure that supports begin() and end(). A list is among the standard-library structures that qualify, so the container requirement is met.

  2. 2
    Check the element requirement

    The element type must be addable to the Value argument. double is one of the element types the source names as valid, so the element requirement is met.

  3. 3
    Decide without rewriting the algorithm

    Both requirements are satisfied, so the same sum() body applies. No separate sum() is needed for lists of double.

Result

sum() is valid for a list of double, and the algorithm itself is unchanged.

Notice what this example did not require. It did not need a separate sum() for vectors of int, another for lists of double, and another for matrices. The algorithm's requirements were the only thing that mattered, and those requirements are stated in terms of what the container and the element must support.

What a Container Holds

Definition

A container is an object that holds elements, and each element is one of the items it holds.

Many programs need to hold more than one value at a time. A list of student scores, a set of file names, or a queue of tasks all share the same need: a single object that holds a group of related items. A container is best understood as an object whose job is to hold other things. Those other things are its elements. The container is the outer thing, and the elements are the items inside it. One container can hold many elements, and the relationship is one to many.

Containerholds a group of elementsElement 1held itemElement 2held itemElement 3held itemholdsholdsholds
How do the container object, its stored elements, and each element's position relate to one another?

Read the diagram from top to bottom. The container sits at the top, and each element hangs beneath it as one of the things it holds. Elements are what a container holds, which is why the two ideas are introduced together in the source.

Range Checking as a Read-Only Question

Definition

Range checking confirms that a requested position falls within the range of elements a container actually holds.

The reason this check matters is that a request for a position is only useful when that position exists. Asking a container for something beyond the elements it holds is asking for something that is not there. Range checking is the habit of verifying the request before relying on its result.

Because out-of-range requests are cases you must handle, you should not assume the element exists. The check gives you the information you need to decide what to do next.

Tracing a Range Check Step by Step

The example below is an original illustration, not taken from the source. It uses a container that holds five elements and numbers them 1 through 5 for the purpose of this example. The source does not fix a numbering scheme, so the numbers here are only a teaching device. The reasoning follows three steps: identify the range, compare the request against it, and decide.

Position 1first elementPosition 2held elementPosition 3held elementPosition 4held elementPosition 5last elementPosition 7requested, not held
Which positions are valid for a container holding five elements, and where does a requested position of 7 fall relative to the first and last elements?
Worked example

Checking a position of 7 in a five-element container

A container holds five elements, numbered 1 through 5. A caller requests position 7. Does the range check pass?

  1. 1
    Identify the range

    The container holds elements at positions 1 through 5. Those are the only positions that refer to elements it actually holds.

  2. 2
    Compare the request against the range

    Position 7 is greater than 5, the position of the last element. It lies beyond the end of the range.

  3. 3
    Decide

    Because 7 falls outside the range of held elements, the range check fails. The request must be handled as an out-of-range case. The container itself is unchanged.

Result

The range check fails. Position 7 is not valid for this container.

Requested positionpIs p at least 1?lower boundaryIs p at most 5?upper boundaryAccept: element heldposition is validReject: out of rangehandle as a casecompareyesnoyesno
What sequence of comparisons does the check perform on a requested index, and at which step does it accept or reject the position?

Now change the request to a position inside the range. Take position 3. The same three steps apply: identify the range of positions 1 through 5, compare 3 against it, and decide. The comparison shows that 3 is at least 1 and at most 5, so the range check passes. The check did not change the container. It only answered whether the request pointed at something the container really holds.

What do you think happens?

Using the same five-element container, will a request for position 0 pass the range check?

  • Yes, because 0 is a small number

  • No, because 0 is below the lowest held position of 1

Reveal answer
Answer:

No, because 0 is below the lowest held position of 1

Under this example's numbering, the held positions are 1 through 5. Position 0 is less than 1, so it fails the lower-boundary comparison in the flowchart and is rejected.

Mistakes With Containers and Ranges

  • Common mistake

    Assuming any type can be passed to a generic algorithm

    What it looks like

    Passing a type that cannot be added to the Value argument to sum() and expecting it to work.

    Why it fails

    Generic does not mean every type is automatically acceptable. The algorithm's requirements must be met.

    Do this instead

    Check whether the element type supports what the algorithm needs before using it.

  • Common mistake

    Believing a separate algorithm is needed for each element type

    What it looks like

    Writing one sum() for int, another for double, and another for Matrix.

    Why it fails

    The algorithm's requirements are what matter. The same body applies to any type that meets them.

    Do this instead

    Write the algorithm once in terms of its requirements and let the type parameter vary.

  • Common mistake

    Thinking the type choice for a template is made while the program runs

    Why it fails

    Templates provide compile-time parametric polymorphism, so the concrete type is tied to compilation.

    Do this instead

    Treat the chosen type as part of what the compiler works with.

  • Common mistake

    Treating a container as if it were one of its elements

    Why it fails

    A container is the outer object that holds elements. The elements are the items inside it.

    Do this instead

    Keep the container and its elements distinct when you reason about a request.

  • Common mistake

    Expecting a range check to fix or remove an out-of-range request

    Why it fails

    Range checking only reads the container's current contents. It does not change them.

    Do this instead

    When a check fails, handle the out-of-range case explicitly rather than assuming the element exists.

Checking Your Own Range Requests

EASY

A container holds eight elements numbered 1 through 8. For each requested position, say whether the range check passes or fails: 0, 4, 8, and 9. Work through the three steps for each one: identify the range, compare the request against it, and decide.

Hints
  • The held positions run from 1 to 8, so a valid request must be at least 1 and at most 8.
  • Position 0 fails the lower comparison, and position 9 fails the upper comparison.
  • The range check never changes the container, so the answers do not depend on any earlier request.
MEDIUM

Explain in your own words why sum() does not need a new definition for a list of double, even though the source describes the element type as independent of the container type.

Hints
  • Ask which two requirements sum() places on its arguments.
  • Think about whether either requirement mentions int specifically.

Key Takeaways

  • Generic programming writes an algorithm once in terms of its requirements, so it can be correct for any type that meets those requirements.
  • In C++, templates provide compile-time parametric polymorphism: the concrete type is supplied and chosen during compilation.
  • sum() is generic in two dimensions, the container type (which must support begin() and end()) and the element type (which must be addable to the Value argument).
  • A container is an object that holds elements, and range checking confirms that a requested position falls within the elements it actually holds without changing the container.

Related concepts

More concepts starting with S