An iterator adapter wraps another iterator and changes what it does. The standard library ships several of them, and they are a good place to see Adapter without a class hierarchy. The target is the iterator protocol. The adaptee is a container, a stream, or another iterator. The adapter is a small value type you can copy and compose.
std::reverse_iterator
std::reverse_iterator<It> stores a forward iterator and flips the traversal direction. operator++ moves the underlying iterator backwards, and dereference reads the element before the stored position.
#include <iostream>
#include <iterator>
#include <vector>
int main() {
std::vector<int> values{1, 2, 3, 4, 5};
for (auto it = std::make_reverse_iterator(values.end());
it != std::make_reverse_iterator(values.begin()); ++it) {
std::cout << *it << ' ';
}
std::cout << '\n';
}
Output:
5 4 3 2 1
The offset matters when you mix a reverse iterator with its base iterator. base() points one past the element the reverse iterator refers to:
std::reverse_iterator<std::vector<int>::iterator> last(values.end());
std::cout << *last << '\n'; // 5
std::cout << *(last.base() - 1) << '\n'; // 5
std::back_insert_iterator
Algorithms write through output iterators with assignment. A container is not an output iterator, but std::back_insert_iterator adapts one. Assignment calls push_back, so the container grows as the algorithm writes.
#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
int main() {
std::vector<int> input{1, 2, 3, 4, 5, 6};
std::vector<int> evens;
std::copy_if(input.begin(), input.end(),
std::back_inserter(evens),
[](int value) { return value % 2 == 0; });
for (int value : evens) {
std::cout << value << ' ';
}
std::cout << '\n';
}
Output:
2 4 6
std::front_inserter calls push_front, so it needs a container that has that operation, such as std::list or std::deque. std::inserter calls insert at a position you supply.
std::move_iterator
Dereferencing a normal iterator gives a reference. Dereferencing std::move_iterator gives an rvalue reference, so algorithms and constructors move instead of copy.
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
int main() {
std::vector<std::string> source{"one", "two", "three"};
std::vector<std::string> target(
std::make_move_iterator(source.begin()),
std::make_move_iterator(source.end()));
for (const auto& value : target) {
std::cout << value << ' ';
}
std::cout << '\n';
}
The strings end up in target, and the elements in source are left in a valid but unspecified state. That is the point of the adapter, and also the reason to use it carefully.
Stream iterators
std::istream_iterator<T> presents formatted input as an input iterator. std::ostream_iterator<T> presents an output iterator that writes to a stream. Between them, algorithms can read and write streams without a manual loop.
#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
int main() {
std::vector<int> values;
std::copy(std::istream_iterator<int>(std::cin),
std::istream_iterator<int>(),
std::back_inserter(values));
std::copy(values.begin(), values.end(),
std::ostream_iterator<int>(std::cout, "\n"));
}
Stream iterators are single-pass. They parse formatted text, so a malformed token puts the stream into a failed state. Check the stream after the copy if the input comes from a user or a file.
The adapter table
| Standard adaptor | Wraps | What changes |
|---|---|---|
std::reverse_iterator<It> |
a bidirectional iterator | traversal runs backwards |
std::back_insert_iterator<C> |
a container with push_back |
assignment appends |
std::front_insert_iterator<C> |
a container with push_front |
assignment prepends |
std::insert_iterator<C> |
a container with insert |
assignment inserts at a position |
std::move_iterator<It> |
any iterator | dereference yields an rvalue reference |
std::istream_iterator<T> |
an input stream | the stream becomes an input iterator |
std::ostream_iterator<T> |
an output stream | assignment writes to the stream |
std::counted_iterator<It> |
an iterator and a count | the iterator carries a bound |
std::common_iterator<I, S> |
an iterator and a sentinel | both become one iterator type |
The last two are C++20. They adapt iterator and sentinel pairs for the ranges library.
Why this is Adapter
The client is an algorithm such as std::copy or std::copy_if. It expects the iterator protocol: increment, dereference, compare, assign. The adaptee is a container, a stream, or another iterator that does not provide that protocol directly. The adapter is the iterator wrapper.
There is no virtual dispatch and no heap allocation in the usual case. The adapter is a template value type, so the compiler can inline the forwarding. That is why iterator adapters are cheap enough to use in tight loops.
Things to watch
reverse_iterator::base()is offset by one. Do not assume it points at the element you just dereferenced.std::back_inserterdoes not reserve capacity. It callspush_back, so astd::vectorcan reallocate during the algorithm.move_iteratorleaves moved-from elements behind. Do not read them as if they still held their old values.- Stream iterators are single-pass and tied to formatted I/O. They are convenient for examples and simple tools, not for binary data.
- Range adaptors such as
std::views::reverseare a separate layer. They compose lazily and behave more like Decorator than Adapter.
Summary
Iterator adapters are the standard library's cleanest Adapter examples. Each one wraps an existing type and makes it satisfy the iterator protocol the algorithms already use. They do not copy the container or the stream, and they do not add a class hierarchy. When you write your own iterator wrapper, follow the same rule: change the interface, not the data.