Concepts / The std::vector Class

The std::vector Class

A Vec class inherits from std::vector and redefines its subscript operations so that they perform range checking.

  • Programming

Why Subscripts Need a Second Look

A std::vector stores its elements in order, and you usually reach one of them with the subscript operator, written v[i]. That syntax is convenient, but it raises a practical question: what happens when i does not name a real element? This article answers that question and shows how a programmer can change the behavior of the subscript without changing the way it is written at the call site.

The solution is a class called Vec. It takes over the subscript operations of std::vector while keeping everything else the same. Read the class carefully once you reach it, and focus on the two operator functions and the single call each of them makes. Those two details carry the whole idea.

Predicting a Bad Index

Before reading the explanation, make a prediction. Suppose you have a Vec<int> holding five values, so the valid indices are 0 through 4. You then write an expression that uses index 10. The Vec operator calls at() internally. Decide what you expect the program to do.

What do you think happens?

What do you think happens when a Vec<int> holding five values is subscripted with index 10?

  • The value at index 10 is returned silently

  • An out_of_range exception is thrown

  • The program stops at compile time

Reveal answer
Answer:

An out_of_range exception is thrown.

The redefined operator forwards index 10 to at(). The at() operation finds that 10 is outside the range 0 to 4 and throws an out_of_range exception instead of yielding an element.

Why a Plain Subscript Does Not Check

The ordinary std::vector subscript operator does not check its index. When you write v[i], the operator takes i and gives you access to the element at that position without first verifying that i is inside the vector's range. The vector does not stop the program with a range error at that point.

The at() operation is different. Calling v.at(i) performs the range check, and it returns the element when the index is valid. When the index is outside the vector's range, at() throws an out_of_range exception. So the two operations share the same job of reaching an element, but only at() reports a bad index in a way the program can catch.

index 010requested index 10outside the range 0 to 4index 120index 230index 340index 450not a stored element
Where does an index like 10 land when the vector holds only five elements, and why does the plain subscript not stop there?

The diagram shows the five valid cells and a requested index that lies outside them. The plain subscript carries no check of its own, so nothing in that access is set up to report the mismatch as an exception. The difference from at() lies entirely in whether the range check happens.

Inheriting from std::vector

Definition

Vec is declared as a template that inherits publicly from std::vector<T>. Inheritance gives Vec all of the vector's members, so Vec does not need to reimplement storage, size, or iteration. The only thing Vec changes is the subscript operations.

Redefining a member function in a derived class replaces the behavior that a caller sees when using the derived object. This is the mechanism that makes range checking possible here. A caller writes v[i] on a Vec object, and the derived class's version of the operator is the one that runs.

Vec<T>redefines operator[]std::vector<T>storage, size, iterationvector at()performs the range checkinherits publiclyoperator[] forwards index
Which class owns the operator that gets called on a Vec object, and how does it reach the base std::vector storage?

Redefining the Subscript Operators

The Vec class makes both kinds of subscript checked. The modifiable operator and the const operator are both redefined, so checking applies whether the object is modifiable or constant. Each redefined operator does not compute the element itself. It hands the index to the vector's at() operation, which performs the check and returns the element when the index is valid.

cpp
Output
30
caught out_of_range
Notes on the code
  • Vec publicly inherits from std::vector<T>, so storage and size come from the base class
  • The using declaration makes the base constructors, including the initializer-list constructor, available to Vec
  • Both operator[] functions forward the index to this->at(i) and do no comparison of their own
  • v[2] is a valid index, so at() returns the element 30
  • v[10] is outside the range 0 to 4, so at() throws out_of_range and the catch block runs

Notice what is absent from the Vec operators. Neither one contains an if statement or a comparison against the size of the vector. The check lives entirely inside at(), so Vec gets range checking by delegation rather than by repeating the logic. The call site v[i] stays the same, but its behavior on a bad index changes.

Tracing a Checked Subscript Call

Follow a checked subscript step by step. The call enters the redefined operator, which forwards the index to at(). The at() operation then decides between two outcomes: it either yields the element or throws an exception. That decision point is the only place where the path can split.

v[i] called on Vecredefined operator[]forwards index ivector at(i)performs the checkis i in range?return elementthrow out_of_rangeentersforwards ichecksyesno
When Vec's subscript is called with an index, which check runs first, which branch leads to the throw, and where does control go from there?

The flowchart has exactly one branch point, and it sits inside at(). When the index is valid, control returns the element back through the redefined operator to the caller. When the index is outside the range, control leaves the normal return path and the out_of_range exception is thrown. The redefined operator never gets to decide the outcome itself.

range checknonerange checkperformedon bad indexno check to report iton bad indexthrows out_of_range
For the same out-of-range index, how do operator[] and at() differ in whether they check, what they do on failure, and what the caller sees?

Mistakes When Reading Vec Failures

  • Common mistake

    Assuming v[i] and v.at(i) behave the same on a bad index

    What it looks like

    Vec<int> holds five values, and the code evaluates v[10] expecting an error.

    Why it fails

    The plain std::vector subscript does not check its index, so the check that at() performs is not part of the plain subscript.

    Do this instead

    Use at() when you need the range check, or use Vec, whose subscripts forward to at().

  • Common mistake

    Looking for a bounds comparison inside the Vec operator

    What it looks like

    Searching the body of operator[] for an if statement that compares i to the size.

    Why it fails

    The redefined operator contains no comparison. It forwards the index to at(), which performs the check.

    Do this instead

    Look inside at() for the decision point that either yields the element or throws.

  • Common mistake

    Expecting the caller to handle the failure at the subscript itself

    What it looks like

    Writing v[10] with no surrounding try block and assuming the value is returned as a default.

    Why it fails

    When the index is outside the range, at() throws an out_of_range exception, and the exception travels to the caller unless it is caught.

    Do this instead

    Wrap the access in a try block that catches std::out_of_range when a bad index is possible.

Checking Your Understanding

MEDIUM

A Vec<int> holds the values 10, 20, 30, 40, 50. Without running the code, state which operator function is called for the expression v[3] on a non-const object, which function performs the range check, and what value the expression produces. Then do the same analysis for v[5].

Hints
  • Look at which operator the non-const object uses, then at what that operator calls.
  • Valid indices are 0 through 4 for this vector.
  • Index 3 is in range, so at() returns an element; index 5 is not.

Key Takeaways

  • A plain std::vector subscript v[i] does not check its index, while v.at(i) performs the range check and throws out_of_range for an index outside the vector's range.
  • Vec inherits publicly from std::vector<T> and redefines both its modifiable and const subscript operators, so the storage and other members come from the base class unchanged.
  • The redefined operators forward the index to at() and contain no comparison of their own, so range checking is obtained by delegation.
  • A checked subscript call has one decision point, inside at(): a valid index returns the element, and an invalid index throws out_of_range.

Related concepts

More concepts starting with T