std::stack, std::queue, and std::priority_queue are container adaptors. The standard uses the word "adaptor" rather than "adapter", but the idea is the same. Each one takes an existing sequence container and exposes a narrower interface with a different contract.
The shape
A container adaptor owns the underlying container and forwards a small set of operations. It does not inherit from the container, and it does not expose the container's iterators or the rest of its interface.
#include <deque>
#include <stack>
#include <vector>
std::stack<int> stack; // std::deque<int> by default
std::stack<int, std::vector<int>> fast; // std::vector<int>
The default container is std::deque<T>. You can supply another one as the second template argument if it meets the adaptor's requirements.
A stack built by hand
The standard std::stack is a good model for your own adapters. It wraps a container, exposes only the operations that make sense for a stack, and lets the caller choose the storage.
#include <cstddef>
#include <deque>
#include <utility>
template<typename T, typename Container = std::deque<T>>
class Stack {
public:
void push(const T& value) {
data_.push_back(value);
}
void push(T&& value) {
data_.push_back(std::move(value));
}
void pop() {
data_.pop_back();
}
T& top() {
return data_.back();
}
const T& top() const {
return data_.back();
}
bool empty() const {
return data_.empty();
}
std::size_t size() const {
return data_.size();
}
private:
Container data_;
};
Usage is the same as with the standard type:
Stack<int> values;
values.push(1);
values.push(2);
values.push(3);
while (!values.empty()) {
std::cout << values.top() << ' ';
values.pop();
}
Output:
3 2 1
Why not inherit from the container
Deriving from std::vector looks shorter, but it exposes the whole vector interface:
class BadStack : public std::vector<int> {
public:
void push(int value) { push_back(value); }
int pop() { int value = back(); pop_back(); return value; }
};
A caller can now call insert, erase, operator[], or take iterators and break the stack invariant. The adapter has stopped adapting anything. It is just a vector with two extra methods. Composition keeps the invariant enforceable.
The three standard adaptors
std::stack gives last-in, first-out access. It needs back, push_back, and pop_back. std::deque is the default because it grows at both ends without invalidating references as often as a vector can.
std::queue gives first-in, first-out access. It needs front, back, push_back, and pop_front. std::deque is again the default. std::list also works.
std::priority_queue keeps the largest element on top by default. It needs random access, front, push_back, pop_back, and the heap operations. std::vector is the default, and a custom comparator changes the order:
#include <functional>
#include <queue>
#include <vector>
std::priority_queue<int> max_heap;
std::priority_queue<int, std::vector<int>, std::greater<int>> min_heap;
| Adaptor | Default container | Requirements |
|---|---|---|
std::stack<T> |
std::deque<T> |
back, push_back, pop_back |
std::queue<T> |
std::deque<T> |
front, back, push_back, pop_front |
std::priority_queue<T> |
std::vector<T> |
random access, front, push_back, pop_back, heap operations |
Writing your own container adapter
Keep the wrapper thin and the contract explicit:
- Store the container by value unless you have a reason to borrow it.
- Accept the container as a template parameter with a sensible default.
- Expose only the operations that preserve the adaptor's invariant.
- Do not hand out iterators unless the adapted interface actually needs them.
- Document what happens when the container throws, especially if the adaptor holds a lock.
The last point matters when the adaptor does more than forward calls. A thread-safe queue, for example, cannot hold a mutex across a call that may block or throw without thinking about ownership and lock order.
When to use a container adapter
Use one when an existing container has the storage you need but the interface is too wide. A stack, a queue, or a bounded buffer should not let callers erase from the middle or walk the storage. The adapter makes that impossible.
Avoid it when callers need the full container interface. Wrapping a vector in a class that exposes begin, end, operator[], insert, and erase gives you the cost of a wrapper with none of the restriction. Use the container directly.
Summary
Container adaptors are object adapters with value semantics. They own a container, forward a small set of operations, and hide the rest. The standard library's stack, queue, and priority queue are good templates for your own wrappers because they show where to stop: expose the contract, not the storage.