AlgoMaster Logo

Iterator Adapters

Low Priority38 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

An iterator adapter is a wrapper that takes an existing iterator (or a container, or a stream) and produces a new iterator whose increment, dereference, or assignment behaviour does something different. The same std::copy call can write into a growing vector, send elements to std::cout, read values from std::cin, or walk a range in reverse, depending on which adapter wraps the source or destination. This chapter covers the four families that come up most often: insert iterators, stream iterators, reverse iterators, and move iterators.

Why Adapters Exist

The STL's separation between algorithms and containers depends on iterators as the glue. An algorithm like std::copy doesn't know whether its destination is a vector, a deque, a std::set, or a file. It writes through an iterator. That design only works if the iterator type can be adapted to whatever the caller wants the writes to do.

The same algorithm composes with different adapters to do very different things. An adapter is a small class that satisfies the iterator interface (*, ++, assignment) but rewrites those operations into something else. Adapters are how the STL turns five or six core algorithms into dozens of useful operations.

The four families covered here:

FamilyAdaptersPurpose
Insert iteratorsback_inserter, front_inserter, inserterGrow a container via algorithm output
Stream iteratorsistream_iterator, ostream_iteratorRead from or write to a stream as if it were a range
Reverse iteratorsrbegin(), rend(), make_reverse_iteratorWalk a bidirectional range backward
Move iteratorsmake_move_iterator, move_iterator<It>Convert element copies into element moves

All four are defined in <iterator>.

Insert Iterators

The default behaviour of writing through an output iterator is to overwrite an existing slot. *dest = value stores into whatever element dest points at. That requires the slot to already exist. Writing past the end of a vector with *v.end() = x is undefined behaviour.

Insert iterators replace the overwrite with a container insertion. Each *it = value calls a member function on the wrapped container, and ++it is a no-op or returns to a state ready for the next insert. The result is that the container grows as the algorithm runs.

std::back_inserter

std::back_inserter(container) returns a std::back_insert_iterator whose assignment calls container.push_back(value). The container must support push_back. std::vector, std::deque, std::list, and std::string all qualify.

Output:

The dest vector starts empty and grows by four elements during the copy. Without the adapter, std::copy(source.begin(), source.end(), dest.begin()) on an empty vector would be undefined behaviour because dest.begin() == dest.end() and there's nothing to write into.

back_inserter is the most common adapter. The pattern std::copy_if(..., std::back_inserter(filtered)) or std::transform(..., std::back_inserter(mapped)) shows up wherever a collection is being filtered or mapped into a new one.

Every push_back may trigger a reallocation. If the output size is known in advance, call dest.reserve(n) before the algorithm to allocate once. A million-element copy without reserve costs roughly log2(n) reallocations.

Output:

std::front_inserter

std::front_inserter(container) is the mirror: each assignment calls container.push_front(value). The container needs push_front, which std::vector does not have. std::deque and std::list are the usual targets.

Output:

The order is reversed because each new element is pushed to the front, so the last element written ends up first. The reversal is the wrapper's contract, not a bug, and it's the main reason front_inserter is less common than back_inserter.

Trying std::front_inserter on a std::vector is a compile error: vector has no push_front.

std::inserter

std::inserter(container, where) is the general form. Each assignment calls container.insert(where, value) and then advances where. It works with any container that supports insert at a position, which covers all sequence containers plus the associative containers (std::set, std::map, std::unordered_set, std::unordered_map).

For associative containers, inserter is the only choice, because they don't have push_back or push_front. The where argument is a hint, and the container ignores it for the hash containers, but inserter still has to be given some iterator (begin() is conventional).

Output:

The std::set discards duplicates and orders elements, so the output is sorted and deduplicated. The algorithm itself didn't change; the destination did.

The three insert iterators compare like this:

AdapterContainer callCommon targetsOrder preserved?
back_inserterpush_backvector, deque, list, stringyes
front_inserterpush_frontdeque, listreversed
inserterinsert(pos, ...)any with insertdepends on container

Quick Check: Which insert iterator would you use to copy elements from a vector into a std::set<int>?

  • A) std::back_inserter
  • B) std::front_inserter
  • C) std::inserter

<details> <summary>Answer</summary>

C. std::set doesn't have push_back or push_front, so the other two are compile errors. std::inserter(s, s.begin()) calls s.insert(...), which is the right interface for any associative container.

</details>

Stream Iterators

Stream iterators bridge the gap between streams and algorithms. They let an algorithm treat std::cin as an input range to read from, or std::cout as an output range to write to. There's a constraint: stream iterators are input/output iterators only, so they can't be used with algorithms that need random access (std::sort won't take an istream_iterator).

std::ostream_iterator

std::ostream_iterator<T>(stream, sep) wraps an output stream and a separator string. Each assignment writes the value followed by the separator to the stream.

Output:

The trailing separator after 422 is part of the contract: every write includes the separator. To avoid the trailing space, write the first element manually and then copy the rest, or use C++20 std::format / ranges, or skip the algorithm entirely and write a normal loop.

The element type doesn't have to be a number. Anything operator<< accepts works.

Output:

The separator can be any string, including "\n", ", ", or "". The T template parameter has to match the value type the iterator will receive.

std::istream_iterator

std::istream_iterator<T>(stream) reads values of type T from the stream, one at a time, on each increment. The default-constructed std::istream_iterator<T>() represents the end-of-stream sentinel. The two together delimit the range.

Output:

Two adapters compose in that one call: istream_iterator on the input side, back_inserter on the output side. The algorithm reads from a stream and writes into a vector without knowing about either: it walks an input range and writes through an output iterator.

The reading stops at end-of-stream or at the first input that fails to parse as T. A stream like "10 20 hello 30" would stop after reading 20, because hello can't be parsed as int. That's both a feature (it terminates cleanly on bad data) and a hazard (partial reads happen without a thrown exception).

Output:

std::accumulate works against the stream-as-range without storing anything in memory. The iterator reads one value at a time, the algorithm adds it to the running total, and the next value is read.

Stream iterators are input iterators only. Each ++ reads from the stream, which involves I/O and parsing. They're fine for one-shot ingestion, but they're not cheap inside a tight loop.

The pipeline above is what makes stream iterators useful. Any algorithm that consumes input iterators and produces output through an output iterator can sit in the middle, treating the streams on either side as ranges.

Reverse Iterators

A reverse iterator walks a range backward. ++ on a reverse iterator moves toward the front of the underlying container; * returns the element it logically points at, which is one before its underlying position.

The standard containers expose them via rbegin() and rend():

  • rbegin() returns a reverse iterator pointing logically at the last element.
  • rend() returns a reverse iterator pointing logically one before the first element.

Output:

The loop reads identically to a forward loop. The only difference is rbegin() / rend() and the fact that ++it now means "move toward the front." That uniform interface is what makes reverse iterators useful: any algorithm that takes a forward iterator pair works on rbegin() / rend() and processes the elements in reverse order.

Output:

Searching in reverse gives "find the last X" with one call. The same approach converts "find the last" or "iterate from newest to oldest" problems into a one-line algorithm call.

Converting Between Forward and Reverse Iterators

A reverse iterator has a base() member that returns the underlying forward iterator. The relationship is offset by one: a reverse iterator pointing logically at element i has a base() that points at element i + 1.

Output:

The off-by-one between rit and rit.base() exists so that the half-open ranges still work. rbegin() corresponds to end(), and rend() corresponds to begin(). Without the offset, rend().base() couldn't equal begin().

The diagram shows the dual-anchoring. The same boundary positions carry two names, one for each iteration direction.

Quick Check: Given std::vector<int> v = {1, 2, 3, 4, 5}, what does *(v.rbegin() + 2) print?

  • A) 1
  • B) 3
  • C) 4

<details> <summary>Answer</summary>

B. v.rbegin() points at 5 (the last element). Adding 2 moves it two positions toward the front in reverse iteration, which is the element 3. Counting from the end: 5 (offset 0), 4 (offset 1), 3 (offset 2).

</details>

make_reverse_iterator

C++14 added std::make_reverse_iterator(it) for the rare case where a reverse iterator needs to be constructed from a forward iterator that isn't begin() or end(). Useful when bounding a reverse traversal somewhere in the middle of a container.

Output:

The iteration walks from index 3 down to index 1, in reverse, which produces 30, 20. Compared to handcrafting a std::reverse_iterator<Iter> template instantiation, make_reverse_iterator keeps the syntax short.

Move Iterators

A move iterator turns assignment-through into move-assignment instead of copy-assignment. Wrapping an iterator with std::make_move_iterator(it) makes *it return an rvalue reference, so any algorithm that writes *dest = *src performs a move rather than a copy.

Output (typical):

For std::string, the move steals the heap allocation from the source, leaving it in a "valid but unspecified" state (most implementations leave it empty). For trivially copyable types like int, the move is identical to a copy, so the wrapper does nothing useful.

Move iterators are most useful when transferring ownership of expensive-to-copy objects between containers, like moving a vector of strings or a vector of std::unique_ptr<T> into another vector. Without the wrapper, std::unique_ptr wouldn't even compile inside std::copy, because it's not copyable.

Output:

Without make_move_iterator, this code wouldn't compile, because std::copy would try to copy each unique_ptr, and unique_ptr deletes its copy constructor. Wrapping the iterators makes the operation a move, which unique_ptr supports.

There's also a dedicated algorithm std::move(first, last, dest) in <algorithm> that does the same thing without needing the adapter explicitly. Both forms are common; std::move (the algorithm) is shorter when the entire range is being moved, while make_move_iterator is more flexible when only part of the range is being moved.

A move is usually cheaper than a copy for types that own heap memory (std::string, std::vector, smart pointers). For int, double, and other trivially copyable types, move and copy are identical, so wrapping is pure overhead.

A Practical Composition

The four adapter families compose well. The program below reads prices from a string stream, filters out invalid ones with copy_if, transforms the valid ones into a label, and prints them in reverse order with no intermediate hand-written loops.

Output:

Four adapters appear in the same program: istream_iterator to read, back_inserter to fill, rbegin() / rend() to reverse, and ostream_iterator to write. Each step is a single algorithm call. No raw loops, no manual index tracking. The pipeline reads like a description of the data flow.

Interview Questions

Q1: What's the difference between `std::back_inserter` and calling `std::copy` with `dest.begin()` directly?

std::copy(src.begin(), src.end(), dest.begin()) overwrites existing slots in dest, so dest must already be sized large enough. If dest is empty, the call writes past the end and triggers undefined behaviour. std::copy(src.begin(), src.end(), std::back_inserter(dest)) calls dest.push_back(value) for each element, growing the container as needed. The first form is faster when the destination is pre-sized (no per-element growth check), but the second handles the unknown-size case safely.

Q2: When would you use `std::inserter` over `std::back_inserter`?

When the target container doesn't support push_back, which means associative containers like std::set, std::map, std::unordered_set, and std::unordered_map. inserter works against any container with a insert(position, value) member, so it covers both sequence containers and associative ones. The trade-off is that inserter is slightly more verbose because it takes an iterator argument, but for set-like containers it's the only option.

Q3: Why does `std::ostream_iterator<int>` need the `<int>` template argument?

Because the iterator has to know how to interpret the value passed to it via *it = value. The template argument fixes the type that gets streamed out. Without it, the iterator couldn't generate the correct stream << value call at compile time. The same applies to istream_iterator<T>, where T tells the iterator what type to read from the stream on each increment.

**Q4: What happens when you call *it on v.rend()?**

Undefined behaviour. rend() is a one-past-the-end sentinel in reverse direction, conceptually positioned one before begin(). It's a valid iterator value for comparison (it != v.rend()) but it doesn't refer to any element. Dereferencing it has the same semantics as dereferencing v.end() in forward iteration: the iterator is past the boundary, and reading through it is not defined.

Q5: When does `std::make_move_iterator` actually save work, and when is it equivalent to a regular copy?

It saves work when the element type has move semantics that are meaningfully cheaper than copy semantics. For std::string, std::vector, std::unique_ptr, or any class that owns heap memory, move-assignment transfers ownership of the underlying allocation in O(1), while copy-assignment allocates and copies. For trivially copyable types like int, double, or a small POD struct, move and copy do the same thing: a byte-wise copy. Using make_move_iterator on those types isn't harmful, but it doesn't help either.

Exercises

Exercise 1: Use std::copy with std::back_inserter to copy the contents of source into dest, then print dest.

Expected Output:

<details> <summary>Solution</summary>

</details>

Exercise 2: Use std::front_inserter with a std::deque<int> to copy {1, 2, 3, 4, 5} and print the result. Predict the output before running it.

Expected Output:

<details> <summary>Solution</summary>

Each push_front puts the new element at the front, so the final order is reversed.

</details>

Exercise 3: Use std::inserter to copy elements from a vector into a std::set<int>, deduplicating along the way.

Expected Output:

<details> <summary>Solution</summary>

</details>

Exercise 4: Use std::ostream_iterator to print a vector of strings, each on its own line.

Expected Output:

<details> <summary>Solution</summary>

</details>

Exercise 5: Use std::istream_iterator to read all int values from a std::istringstream containing "10 20 30 40", and store them in a vector. Print the count and the contents.

Expected Output:

<details> <summary>Solution</summary>

</details>

Exercise 6: Use a reverse iterator to print the last three elements of a vector in reverse order. The vector is {10, 20, 30, 40, 50}.

Expected Output:

<details> <summary>Solution</summary>

</details>

Exercise 7: Use std::find_if with reverse iterators to find the last even number in a vector {1, 4, 7, 8, 3, 10, 11} and print it.

Expected Output:

<details> <summary>Solution</summary>

Walking in reverse finds the last matching element in one call.

</details>

Exercise 8: Fix the bug in this code. The intent is to copy source into dest in reverse order.

Expected Output (after fix):

<details> <summary>Solution</summary>

The bug is dest.begin(): dest is empty, so writing through dest.begin() is undefined behaviour. The fix is std::back_inserter(dest).

</details>

Exercise 9: Use std::make_move_iterator to move all std::string elements from source into dest. After the move, print both vectors.

Expected Output (typical):

<details> <summary>Solution</summary>

Post-move the source strings are valid but unspecified; on most implementations they're empty.

</details>

Exercise 10: Write a pipeline that reads doubles from a std::istringstream, keeps only those above 10.0, and prints them in reverse order separated by ", ". Use istream_iterator, back_inserter, and rbegin()/rend() together.

Input: "5.5 12.0 7.3 18.8 9.0 25.5"

Expected Output:

<details> <summary>Solution</summary>

</details>

Quiz

Iterator Adapters Quiz

10 quizzes