Question
While iterating over a std::vector, I need the zero-based index of the element currently referenced by an iterator. What are the advantages and disadvantages of these two approaches?
it - vec.begin();
std::distance(vec.begin(), it);
Short Answer
You will learn how to convert an iterator position into an offset, why both expressions are efficient for std::vector, and why std::distance works with more iterator types. You will also learn the complexity and validity rules that determine which form is appropriate.
Concept
An iterator is an object that identifies a position in a container or range. To find an iterator's position relative to the beginning, calculate the number of steps from begin() to that iterator.
For a vector, both of these expressions produce that offset:
it - vec.begin();
std::distance(vec.begin(), it);
A std::vector has random-access iterators, which can jump directly to a position. Therefore, both operations take constant time, O(1), for a vector.
The important difference is generality:
it - vec.begin()requires random-access iterators.std::distance(vec.begin(), it)works with many iterator categories, including iterators fromstd::listandstd::forward_list.
For non-random-access iterators, std::distance may need to walk one element at a time, making it O(n). Iterator subtraction is not supported for those iterator types at all.
Both expressions return an iterator's difference_type, which is usually a signed integer-like type. It represents a distance or offset, not necessarily a container's unsigned size_type.
Mental Model
Think of iterators as ways of locating a house on a street.
- Iterator subtraction is like using house numbers on a perfectly numbered street: subtract house number 1 from house number 8 and immediately get 7. This only works on streets that support that numbering system.
std::distanceis like asking, “How many steps are there from this house to that house?” On a numbered street, the answer is immediate. On a winding path, you may need to walk and count each step.
A vector is the numbered street, so both approaches are fast. A linked list is the winding path, so only std::distance is generally available, and it may have to walk through nodes.
Syntax and Examples
Use iterator subtraction when you specifically require random-access iterators, such as iterators from std::vector, std::deque, or std::array.
#include <cstddef>
#include <vector>
std::vector<int> values{10, 20, 30, 40};
auto it = values.begin() + 2;
std::vector<int>::difference_type index = it - values.begin();
// index is 2
Use std::distance when you want code that can also work with other iterator categories.
#include <iterator>
#include <list>
std::list<int> values{10, 20, 30, 40};
auto it = std::next(values.begin(), 2);
auto index = std::distance(values.begin(), it);
// index is 2
For a vector, the following are equivalent in result and asymptotic performance:
auto index1 = it - values.begin();
auto index2 = std::distance(values.begin(), it);
If an API requires std::size_t, convert only after you know the result cannot be negative:
std::size_t index = static_cast<std::size_t>(it - values.begin());
When it is a valid iterator in values at or after values.begin(), the conversion is safe.
Step by Step Execution
Consider this vector traversal:
#include <iostream>
#include <iterator>
#include <vector>
int main() {
std::vector<char> letters{'A', 'B', 'C'};
for (auto it = letters.begin(); it != letters.end(); ++it) {
auto index = std::distance(letters.begin(), it);
std::cout << index << ": " << *it << '\n';
}
}
Execution trace:
- On the first iteration,
itequalsletters.begin()and points to'A'. - The distance from
begin()to itself is0, so the program prints0: A. ++itmoves to'B'.- The distance from
begin()to this position is1, so it prints1: B. ++itmoves to'C'.- The distance is now
2, so it prints2: C. - After the final increment,
it == letters.end(), and the loop stops.
Because letters is a vector, std::distance uses a constant-time calculation here. Replacing the distance call with it - letters.begin() produces the same output.
Real World Use Cases
Iterator distances are useful when an algorithm gives you an iterator but another part of the program needs a position.
- Search results: Convert the iterator returned by
std::findinto the index of a matching item in a vector. - Parsing: Report the character offset where a parser found an invalid token.
- User interfaces: Turn a selected iterator in an in-memory list of rows into a row number.
- Data processing: Identify the position of a maximum or minimum element returned by an algorithm.
- Debugging and logs: Include an element offset in an error message.
Example with std::find:
#include <algorithm>
#include <iterator>
#include <vector>
std::vector<int> ids{42, 71, 99};
auto found = std::find(ids.begin(), ids.end(), 71);
if (found != ids.end()) {
auto index = std::distance(ids.begin(), found);
// index is 1
}
Always check for end() before dereferencing a search result. Computing the distance to end() is valid for a vector, but *found would be invalid when the value was not found.
Real Codebase Usage
In production C++ code, choose the expression that communicates the requirement of the code.
Generic algorithms
If a function accepts a general iterator type, prefer std::distance:
#include <iterator>
template <class Iterator>
auto offset_from_begin(Iterator first, Iterator current) {
return std::distance(first, current);
}
This can work with vectors, lists, and other iterator-based ranges, although its cost depends on the iterator category.
Vector-specific code
When code explicitly works with a vector and performance-sensitive indexing is intended, subtraction is common and direct:
std::vector<int> values{4, 8, 15, 16};
auto it = std::find(values.begin(), values.end(), 15);
if (it != values.end()) {
auto index = it - values.begin();
}
Avoid repeated linear distances
For a std::list, do not repeatedly call std::distance(begin, it) inside a traversal if you need a counter. Keep one yourself:
#include <cstddef>
#include <list>
std::list<int> values{10, 20, 30};
std::size_t index = 0;
for (auto it = values.begin(); it != values.end(); ++it, ++index) {
// Use index and *it.
}
This keeps the full traversal O(n), rather than potentially turning it into O(n²).
Common Mistakes
Subtracting list iterators
std::list iterators are bidirectional, not random access. This does not compile:
#include <list>
std::list<int> values{1, 2, 3};
auto it = values.begin();
auto index = it - values.begin(); // Error: operator- is not supported
Use std::distance instead:
auto index = std::distance(values.begin(), it);
Assuming std::distance is always O(1)
For a vector, it is O(1). For a list or forward list, it is normally O(n). Avoid calculating it repeatedly in loops over non-random-access containers.
Mixing iterators from different containers
Do not subtract unrelated iterators:
std::vector<int> left{1, 2};
std::vector<int> right{3, 4};
auto bad = left.begin() - right.begin(); // Undefined behavior
Both iterators must refer to positions in the same underlying sequence or range.
Dereferencing end()
end() is a valid boundary position, but it does not point to an element:
auto it = values.end();
auto index = it - values.begin(); // Valid for a vector
// *it; // Invalid: never dereference end()
Using an unsigned type too early
Distances can be signed. Keep the result as difference_type or auto unless you have verified it is non-negative:
auto offset = it - values.begin();
if (offset >= 0) {
auto index = static_cast<std::size_t>(offset);
}
Invalidating an iterator before using it
Operations such as push_back, insert, erase, or reallocation can invalidate vector iterators. Do not calculate an offset from an iterator after an operation that invalidated it.
Comparisons
| Approach | Supported iterator types | Typical complexity for std::vector | Typical complexity for std::list | Best use |
|---|---|---|---|---|
it - first | Random-access iterators only | O(1) | Not supported | Container-specific code using vectors, deques, or arrays |
std::distance(first, it) | Input, forward, bidirectional, and random-access iterators | O(1) | O(n) | Generic iterator-based code |
| Manual counter in a loop | Any iterable range | O(1) extra work per iteration | O(1) extra work per iteration | You already traverse the range and need each position |
std::vector::size() is also O(1), but it answers a different question: it gives the total number of elements, not the position of a particular iterator.
In modern range-based code, std::ranges::distance is a related C++20 facility. It can be used with iterators or ranges, but the same central idea remains: the cost depends on what the range or iterator category supports.
Cheat Sheet
// Offset of an iterator within a vector
std::vector<int> values{1, 2, 3};
auto it = values.begin() + 1;
auto a = it - values.begin(); // 1; vector only
auto b = std::distance(values.begin(), it); // 1; more general
- Both forms are O(1) for
std::vector. operator-requires random-access iterators.std::distancesupports more iterator types.std::distancecan be O(n) for lists and forward lists.- The result type is usually
Container::difference_type, which is signed. - Use iterators from the same container/range only.
- Never dereference
container.end(). - Check search results against
end()before dereferencing. - If you need positions during a list traversal, keep a separate incrementing counter.
FAQ
Is std::distance slower than subtracting iterators for a vector?
No in asymptotic terms. For vector iterators, both are constant-time O(1). In normal optimized builds, std::distance is typically implemented using subtraction for random-access iterators.
Should I use std::distance or it - begin() with std::vector?
Use subtraction when the code is intentionally vector-specific and you want to state that random access is required. Use std::distance when you want the code to work with a broader set of iterator types.
Does std::distance work with std::list?
Yes. It counts through the list from the first iterator to the second iterator, which is generally O(n).
Why does it - begin() fail for a list?
A list stores elements as linked nodes rather than contiguous positions. Its iterators can move one step at a time, but cannot directly calculate the number of nodes between two positions.
What type does std::distance return?
It returns the iterator's difference_type, a signed integer-like type designed to represent distances between iterator positions.
Can I use the result as a vector index?
Yes, when the iterator is known to be in the vector and at or after begin(). Convert to std::size_t only if an unsigned index is required.
Is calculating the distance to end() valid?
Yes. For a vector, std::distance(vec.begin(), vec.end()) equals vec.size(). However, you must not dereference vec.end().
Mini Project
Description
Build a small lookup utility for a vector of task names. The utility searches for a requested task and reports its zero-based position. This demonstrates safely converting a vector iterator returned by std::find into an index.
Goal
Find a task in a std::vector<std::string> and print its index, or print a clear message if it is absent.
Requirements
Use std::vector<std::string> to store at least four task names.
Search for one task using std::find.
Check whether the search result equals end() before using it.
Calculate the found task's index with iterator subtraction or std::distance.
Print the task name and its zero-based index.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.