A queue is a FIFO data structure: first in, first out.
In C++, there are three related types you’ll see a lot:
std::vector(a real container, contiguous)std::deque(a real container, segmented storage, fast at both ends)std::queue(NOT a container; an adaptor on top of another container, default isdeque)
✅ Contiguous memory
- Elements live in one continuous block.
push_backis amortized O$1$.push_frontdoes not exist (for a reason).- If you simulate
push_frontusinginsert(begin(), x)→ all elements are moved → O$n$.
That’s exactly why vector is not used as a queue.
std::vector
| 1 | 2 | 3 | 4 | 5 |
Insert at front:
| X | 1 | 2 | 3 | 4 | 5 | ← everything moved ❌
❌ Not a container itself
std::queue is a container adaptor.
This:
std::queue<int> q;does NOT mean:
queuehas its own storage- or
queueis a real container likevector
Instead, it means:
std::queuewraps another container and exposes a restricted FIFO interface.
template<
class T,
class Container = std::deque<T>
>
class queue;So:
std::queue<int> q;is equivalent to:
std::queue<int, std::deque<int>> q;That is what “actually std::deque<int> underneath” means.
std::queue:
- stores elements inside its underlying container
- forwards operations to that container
- hides operations that would break FIFO
Example:
q.push(10); // calls deque.push_back(10)
q.pop(); // calls deque.pop_front()
q.front(); // calls deque.front()You cannot:
q[3]; // ❌ no random access
q.push_front(5); // ❌ forbiddenEven though std::deque supports them.
Why not just use std::deque directly?
Because std::queue enforces intent:
If you expose a deque, someone can:
- pop from the back
- insert in the middle
- break FIFO invariants
If you expose a queue, FIFO is enforced by the type system.
This is API design, not performance.
As long as it supports:
push_backpop_frontfrontback
Example:
std::queue<int, std::list<int>> q;But this is rare — deque is almost always the best choice.
- Memory layout
queuememory layout = underlying container memory layout- Default → non-contiguous, because default is
deque
- Complexity
push→ O$1$pop→ O$1$
- You cannot rely on contiguity This is invalid:
int* p = &q.front(); // ❌ meaningless assumption about contiguityThink of std::queue as:
class queue {
private:
std::deque<T> data;
public:
void push(const T&);
void pop();
T& front();
};It’s composition, not inheritance.
std::queue is not a container, it is a restricted interface on top of (by default) std::deque, enforcing FIFO.
❌ NOT contiguous
- Internally: multiple fixed-size blocks
- Plus a small index structure pointing to those blocks
- Elements are not stored in one contiguous chunk
✅ push_back → O$1$
✅ push_front → O$1$
❌ No shifting of existing elements
❌ Not as cache-friendly as vector
push_front does NOT move all elements.
Instead:
- a new block may be allocated (if needed)
- front pointer/index is adjusted
std::deque
Block A: | 1 | 2 |
Block B: | 3 | 4 |
Block C: | 5 |
Push front:
Block X: | X |
Block A: | 1 | 2 |
Block B: | 3 | 4 |
Block C: | 5 |
Only pointers/indices change ✅
| Container | Contiguous | push_back | push_front | Moves elements? |
|---|---|---|---|---|
std::vector |
✅ Yes | O$1$ amort | ❌ O$n$ | ❌ Yes |
std::deque |
❌ No | O$1$ | O$1$ | ✅ No |
std::queue |
❌ No | O$1$ | O$1$ | ✅ No |
vector→ fast iteration, cache-friendly, no front operationsdeque→ real double-ended queuequeue→ FIFO abstraction, usually backed bydeque- Need push/pop at both ends →
deque - Need contiguous memory →
vector
// Job/task queue system
std::deque<Job> jobQueue;
void processNextJob() {
if (!jobQueue.empty()) {
Job& currentJob = jobQueue.front(); // Get next job
currentJob.execute();
jobQueue.pop_front(); // Remove after processing
}
}
void addUrgentJob(const Job& job) {
jobQueue.push_front(job); // High priority at front
}
void addBackgroundJob(const Job& job) {
jobQueue.push_back(job); // Low priority at back
}// Algorithm for sliding window maximum (LeetCode 239)
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> dq; // Stores indices, front has max for current window
vector<int> result;
for (int i = 0; i < nums.size(); i++) {
// Remove elements outside window from front
if (!dq.empty() && dq.front() == i - k) {
dq.pop_front();
}
// Maintain decreasing order in deque
while (!dq.empty() && nums[dq.back()] <= nums[i]) {
dq.pop_back();
}
dq.push_back(i);
// Get max from front for current window
if (i >= k - 1) {
result.push_back(nums[dq.front()]); // Current window maximum
}
}
return result;
}class LRUCache {
private:
list<pair<int, int>> cache; // (key, value)
unordered_map<int, list<pair<int, int>>::iterator> map;
int capacity;
public:
int get(int key) {
if (map.find(key) == map.end()) return -1;
// Move accessed item to front (most recently used)
cache.splice(cache.begin(), cache, map[key]);
return cache.front().second; // Return value from front
}
void put(int key, int value) {
if (map.find(key) != map.end()) {
// Update existing key
cache.splice(cache.begin(), cache, map[key]);
cache.front().second = value; // Update value at front
return;
}
if (cache.size() == capacity) {
// Remove least recently used from back
int lruKey = cache.back().first;
map.erase(lruKey);
cache.pop_back();
}
// Add new item to front
cache.emplace_front(key, value);
map[key] = cache.begin();
}
};class TextEditor {
std::deque<std::string> undoStack;
std::deque<std::string> redoStack;
std::string currentText;
public:
void type(const std::string& text) {
undoStack.push_back(currentText); // Save state
currentText += text;
redoStack.clear(); // Clear redo on new action
}
void undo() {
if (!undoStack.empty()) {
redoStack.push_back(currentText); // Save for redo
currentText = undoStack.back(); // Get previous state
undoStack.pop_back();
}
}
void redo() {
if (!redoStack.empty()) {
undoStack.push_back(currentText); // Save for undo
currentText = redoStack.back(); // Get next state
redoStack.pop_back();
}
}
std::string getText() { return currentText; }
};// Simple priority system where front has highest priority
template<typename T>
class PriorityBuffer {
std::deque<T> highPriority;
std::deque<T> normalPriority;
public:
void addHighPriority(const T& item) {
highPriority.push_back(item);
}
void addNormalPriority(const T& item) {
normalPriority.push_back(item);
}
T getNext() {
if (!highPriority.empty()) {
T item = highPriority.front(); // High priority first
highPriority.pop_front();
return item;
} else if (!normalPriority.empty()) {
T item = normalPriority.front(); // Then normal
normalPriority.pop_front();
return item;
}
throw std::runtime_error("Buffer empty");
}
const T& peekNext() const {
if (!highPriority.empty()) {
return highPriority.front(); // Peek without removing
} else if (!normalPriority.empty()) {
return normalPriority.front();
}
throw std::runtime_error("Buffer empty");
}
};class FileBuffer {
std::deque<std::string> lines;
public:
void loadFile(const std::string& filename) {
std::ifstream file(filename);
std::string line;
while (std::getline(file, line)) {
lines.push_back(line);
}
}
std::string& getFirstLine() {
if (lines.empty()) throw std::runtime_error("File empty");
return lines.front();
}
std::string& getLastLine() {
if (lines.empty()) throw std::runtime_error("File empty");
return lines.back();
}
void appendLine(const std::string& line) {
lines.push_back(line);
}
void prependHeader(const std::string& header) {
lines.push_front(header);
}
};// Processing streaming sensor data (keep only last N samples)
class SensorDataBuffer {
std::deque<double> buffer;
size_t maxSize;
public:
SensorDataBuffer(size_t size) : maxSize(size) {}
void addSample(double value) {
if (buffer.size() >= maxSize) {
buffer.pop_front(); // Remove oldest
}
buffer.push_back(value); // Add newest
}
double getLatest() const {
if (buffer.empty()) return 0.0;
return buffer.back(); // Most recent sample
}
double getOldestInWindow() const {
if (buffer.empty()) return 0.0;
return buffer.front(); // Oldest in current window
}
// Calculate moving average
double getMovingAverage() const {
if (buffer.empty()) return 0.0;
double sum = 0.0;
for (double val : buffer) sum += val;
return sum / buffer.size();
}
};class BrowserHistory {
std::deque<std::string> history;
int currentIndex = -1;
public:
void visit(const std::string& url) {
// Clear forward history when visiting new page
while (history.size() > currentIndex + 1) {
history.pop_back();
}
history.push_back(url);
currentIndex++;
}
std::string back() {
if (currentIndex > 0) {
currentIndex--;
return history[currentIndex]; // Get from "middle" using front/back logic
}
return history.front(); // Return first if at beginning
}
std::string forward() {
if (currentIndex < history.size() - 1) {
currentIndex++;
return history[currentIndex];
}
return history.back(); // Return last if at end
}
};Use front() / back() when:
- you need direct access to first/last elements
- implementing FIFO/LIFO structures
- maintaining sliding windows
- accessing boundaries of data
- simple queue/deque operations
Use begin() / end() when:
- you need to iterate through elements
- using STL algorithms
- working with ranges
- need iterator arithmetic
- generic programming
The choice depends on whether you need element access (front/back) or position/ranges (begin/end).