The essence of a container adapter lies in the principle of reuse. Instead of implementing storage structures from scratch, these adapters leverage existing containers to handle data storage while exposing only the interfaces relevant to their specific access patterns. This adapter pattern represents a fundamental design philosophy in software engineering where existing functionality is wrapped to serve a new purpose without modification to the original implementation.
It is important to note that neither stack nor queue provides iterator support. This intentional limitation prevents arbitrary traversal, ensuring that users interact with these adapters strictly according to their defined access semantics—LIFO for stack and FIFO for queue.
A related concept worth understanding is the Reverse Polish Notation (postfix expression). Standard arithmetic expressions use infix notation, which requires parsing precedence rules. Computers typically convert infix expressions to postfix notation, where operands maintain their original order while operators are rearranged according to precedence rules, eliminating the need for parentheses and simplifying evaluation.
Another practical observation: compilers typically perform forward lookups rather than backward searches. This directional preference optimizes compilation performance by establishing clear scoping and resolution rules that minimize ambiguity during the parsing phase.
Basic Usage of stack and queue
The template declarations for these adapters reveal their flexible nature:
template <class T, class Container = deque<T>> class stack;
template <class T, class Container = deque<T>> class queue;
Unlike other containers where the second template parameter typically specifies a memory allocator, container adapters use this parameter to designate the underlying storage container. The default choice, deque, provides an optimal balance of performance characteristics for most use cases.
stack Operations
The following example demonstrates fundamental stack operations:
std::stack<int> dataStructure;
dataStructure.push(10);
dataStructure.push(20);
dataStructure.push(30);
dataStructure.push(40);
dataStructure.push(50);
std::cout << "Element count: " << dataStructure.size() << std::endl;
while (!dataStructure.empty())
{
std::cout << dataStructure.top() << " ";
dataStructure.pop();
}
std::cout << std::endl;
queue Operations
Queue operations follow a first-in-first-out pattern:
std::queue<int> dataStructure;
dataStructure.push(100);
dataStructure.push(200);
dataStructure.push(300);
dataStructure.push(400);
dataStructure.push(500);
std::cout << "Element count: " << dataStructure.size() << std::endl;
while (!dataStructure.empty())
{
std::cout << dataStructure.front() << " ";
dataStructure.pop();
}
std::cout << std::endl;
Implementing Custom stack and queue Adapters
Creating container adapters requires underlying containers that expose both front and back access points. The Standard Library ensures that both vector and list provide these necessary interfaces, enabling them to serve as potential storage backends.
Template parameters support default values, allowing users to specify custom containers while maintaining sensible defaults for common scenarios.
Performance consideration: Using vector as the underlying storage for a queue would require element repositioning during front removal operations, resulting in O(n) complexity for eacch pop. The STL implementation deliberately avoids this inefficiency by not supporting vector-based queue adapters. Instead, it relies on deque's efficient pop_front capability.
Custom stack Implementation
namespace ContainerAdapters
{
template <typename ElementType, typename StorageContainer = std::deque<ElementType>>
class Stack
{
public:
void push(const ElementType &value)
{
storage_.push_back(value);
}
void pop()
{
storage_.pop_back();
}
const ElementType &top() const
{
return storage_.back();
}
std::size_t count() const
{
return storage_.size();
}
bool isEmpty() const
{
return storage_.empty();
}
private:
StorageContainer storage_;
};
}
Custom queue Implementation
namespace ContainerAdapters
{
template <typename ElementType, typename StorageContainer = std::deque<ElementType>>
class Queue
{
public:
void push(const ElementType &value)
{
storage_.push_back(value);
}
void pop()
{
storage_.pop_front();
}
const ElementType &front() const
{
return storage_.front();
}
const ElementType &back() const
{
return storage_.back();
}
std::size_t count() const
{
return storage_.size();
}
bool isEmpty() const
{
return storage_.empty();
}
private:
StorageContainer storage_;
};
}
Understanding deque (Double-Ended Queue)
The name "deque" can be misleading—it is not simply a queue but rather a sophisticated container supporting efficient insertions and deletions at both ends. This bidirectional access capability makes it uniquely suited as the default storage backend for both stack and queue adapters.
Unlike vector, deque provides random-access iterators while maintaining efficient boundary operations. However, it lacks a reserve() method; instead, it manages expansion through an internal mechanism that allocates new buffer segments as needed, avoiding the wholesale reallocation characteristic of vector growth.
Core deque Interfaces
// Element Access
operator[]
at()
front()
back()
// Modification Operations
assign()
push_back()
push_front()
pop_back()
pop_front()
insert()
erase()
swap()
clear()
emplace()
emplace_front()
emplace_back()
Comparative Analysis: vector, list, and deque
vector strengths: Supports random access via subscript notation, enabling O(1) element retrieval. The contiguous memory layout facilitates CPU cache prefetching, resulting in excellent memory locality and cache utilization during sequential access patterns.
vector limitations: Resizing requires reallocation, data copying, and memory deallocation—a costly operation. Insertion or deletion at arbitrary positions shifts subsequent elements, creating O(n) complexity for such operations.
list strengths: Elements can be inserted or removed anywhere in constant time, as each node is independently allocated. This distributed allocation pattern provides predictable performance regardless of container size.
list limitations: No subscript operator support, requiring traversal for positional access. Individual node allocations increase memory overhead and reduce cache efficiency during sequential traversal.
deque Internal Architecture
The deque implementation employs a sophisticated two-level memory management strategy. The structure maintains multiple fixed-size buffer segments, each representing a contiguous memory block. These buffer addresses are stored in a central control array—a dynamic array of pointers.
When the control array exhausts its capacity, it undergoes reallocation. Unlike vector's element-wise copying, deque expansion requires only copying pointer values, significantly reducing the cost of structural growth.
Buffer expansion follows a bidirectional growth model. When inserting at the front, new buffers are allocated before the first existing buffer, with the control array's starting pointer adjusted accordingly. Similarly, rear expansion appends new buffers to the control array's end.
Iterator design complexity: Deque iterators maintain four critical pointers: first (buffer start), last (buffer end), cur (current position), and node (pointer to control array entry). This complex structure enables navigation across buffer boundaries while maintaining the illusion of contiguous memory. Increment operations leverage all four pointers to determine when buffer transitions are necessary.
Buffer index calculation: Access operations require determining which buffer segment contains the target element. The algorithm first checks if the requested index falls within the first buffer. If not, it subtracts the first buffer's size, then performs division and modulo operations using the fixed buffer capacity to locate the appropriate segment and offset.
Performance Characteristics
Compared to vector, deque dramatically improves head insertion and deletion performance while eliminating expensive reallocation events. However, random-access operations require additional computational overhead for bufffer and offset calculations.
Compared to list, deque provides random-access capabilities and superior cache utilization through larger memory blocks. However, mid-container insertions and deletions present challenges: either extensive element shifting (high cost) or targeted buffer expansion (complicates index calculations and violates the fixed-size invariant that enables efficient indexing). The standard library implementation opts to maintain the fixed-buffer design, accepting reduced mid-container modification efficiency.
practical Applications
The primary use case for deque is serving as the default underlying container for stack and queue adapters. This choice reflects a careful balance of performance characteristics: deque handles frequent head and tail insertions with minimal overhead, avoids vector's reallocation penalties during front operations, and provides better cache utilization than list's fragmented allocation pattern.
For stack operations (push/pop at one end), vector would suffice but risks occasional reallocation events. For queue operations requiring efficient removal from both ends, deque's architecture provides optimal performance without the cache inefficiencies inherent in list's node-per-element approach.