AlgoMaster Logo

std::stack

High Priority16 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

std::stack<T> is a last-in, first-out (LIFO) container adapter from the standard library. It is not a new container in its own right; it is a thin wrapper around another container (a std::deque by default) that exposes only the operations a stack needs. This chapter covers what std::stack is, the small API it offers, why some of its methods look the way they do, how to swap out the underlying container, and the pitfalls that arise when it is treated like a std::vector.

What a Container Adapter Is

A container adapter is a class template that takes an existing container and restricts its interface to fit a specific access pattern. std::stack adapts a sequence container into the LIFO pattern: add and remove happen at one end (the top), and only that one end can be inspected.

The standard library ships three adapters: std::stack (LIFO), std::queue (FIFO), and std::priority_queue (max-heap). The next chapter covers the other two. They all share the same idea: reuse an existing container and hide everything that does not belong to the pattern.

The declaration of std::stack from <stack>:

The second template parameter is the underlying container. The default is std::deque<T>, but std::vector<T> or std::list<T> can be substituted given a reason. The choice of underlying container returns at the end of this chapter.

Because std::stack is an adapter and not a container, it has no iterators, no operator[], no find, no begin/end. The only operations are push to the top, pop from the top, and look at the top. Everything else is deliberately absent.

The diagram shows the adapter pattern. The stack object holds a std::deque inside it and forwards a small set of operations to it. Everything else the deque can do is hidden, on purpose.

The LIFO Picture

Stacks model anything where the most recent item added is the next item taken out. Plates stacked at a buffet, browser back history, the call stack of a running program, undo history in a text editor. The last item in is the first one out.

The vocabulary is fixed: the end where items are added and removed is the top. Adding is push, removing is pop, peeking without removing is top (the noun form).

The diagram tracks a stack through four operations. Each push puts a new item on top of whatever was there. The single pop at the end removes the most recent item, so B is now back on top. The items underneath never move and cannot be reached without popping the top first.

In an e-commerce setting, this matches an undo log for cart edits. Every time the user changes the cart (adds an item, changes a quantity, applies a discount), the previous cart state is pushed onto an undo stack. When the user clicks "undo," the most recent state is popped and restored.

Pushing, Peeking, and Popping

The basic API is five member functions and two queries:

MemberWhat it doesComplexity
push(x)Copies or moves x onto the topO(1) amortised
emplace(args...)Constructs the new element in place on the topO(1) amortised
top()Returns a reference to the top element (does not remove it)O(1)
pop()Removes the top element. Returns void.O(1)
size()Number of elements currently heldO(1)
empty()true when there are no elementsO(1)

That is the entire surface area. No iteration, no random access, no search.

A minimal undo log for a shopping cart: each cart edit pushes a snapshot of the cart's total. An "undo" pops the most recent snapshot and rolls back to the previous total.

The program reads cleanly: no loop counter, no index, no iterator. Every cart change pushes a state, every undo pops the latest state. That is the LIFO pattern.

push and pop on std::stack are amortised O(1) because the default underlying container is a std::deque, which appends and removes at the back in constant time on average.

The emplace member constructs the new element directly in the stack's storage, skipping a copy or move. It mirrors vector::emplace_back and related members. For a non-trivial type like a struct or class, emplace is the cheaper choice.

Both push and emplace call the underlying container's push at the back, so the cost is the same. The difference is at the call site: push constructs a temporary then moves it in, while emplace forwards its arguments to the constructor directly. For small types this is a stylistic difference, but for objects with non-trivial constructors, emplace avoids one move.

Why pop() Returns void

A reasonable first reaction to the API: why does pop not return the element it removed? In most languages with a stack type, it does. C++ deliberately splits the two operations: top() returns the element by reference, and pop() removes it and returns nothing.

The reason is exception safety. A hypothetical pop() that returned the popped value by value:

For this to work, pop would need to remove the element from the stack and then return a copy or move of it. If the copy or move constructor of T throws, the element is already removed from the stack, but the caller never receives the return value. The element is gone, and nothing was returned to the caller. That is a data loss bug the language cannot undo.

By splitting the operation in two, the standard library puts both halves in a state that can fail independently without losing data:

  1. top() returns a reference. Copying that reference into a variable, even if the copy throws, leaves the element on the stack. The caller can retry or recover.
  2. pop() then removes the element. It returns void, so nothing is constructed, and nothing can throw on the way back out (assuming the destructor of T does not throw, which the standard requires).

The idiomatic pattern is two lines:

To move the value out (more common for non-trivial types):

This is not a quirk of std::stack alone; std::queue and std::priority_queue make the same choice for the same reason.

A small example uses the two-step pattern to process undo log entries:

The order: the newest entry comes out first, the LIFO promise. The loop drains the stack until empty() returns true.

Calling top() or pop() on an Empty Stack

This is a common bug with std::stack. Both top() and pop() on an empty stack are undefined behaviour. They are not bounds-checked, they do not throw an exception, they do not return a sentinel value. The standard says the result is undefined, which means the program may crash, may read garbage, may appear to work in debug builds and corrupt memory in release builds.

The bug:

On g++ with the default deque-backed stack, the result is typically a segmentation fault or a garbage value, but the actual behaviour depends on the optimiser and the libstdc++ implementation. Either way, the program is broken.

The fix is to check empty() before reading or removing:

The two safe patterns are "check empty() first" and "loop while !empty()". Anything else risks reading past the end of the underlying container.

empty() is O(1). There is no reason to skip the check; the cost is negligible compared to the cost of debugging a UB bug that surfaces only in deployment.

This pairs cleanly with the loop pattern from earlier. Drain a stack with while (!s.empty()) to avoid reading past the end.

A Real Example: Balanced Brackets

The classic textbook use of a stack is checking whether brackets in a string are balanced. Every opening bracket goes onto a stack; every closing bracket must match the top of the stack. If the stack is empty when a closing bracket appears, or non-empty at the end, the string is unbalanced.

In an e-commerce setting, consider validating a saved product specification that uses nested brackets for variants (color groups, size groups, options). The same algorithm works.

The first string is balanced. The second has an opening [ that never gets closed, so the stack still has an entry when the loop ends. The third has an extra ], so when the parser sees it, the stack is empty and the function returns false immediately.

This is the canonical stack problem because the LIFO order matches the language structure. The most recently opened bracket must be the next one closed. A queue or a list does not fit; the stack does.

Expression Evaluation in Brief

Another standard use is evaluating postfix (reverse Polish) expressions, which is how some calculator engines and bytecode interpreters work. Push operands onto a stack; on encountering an operator, pop the two top values, compute, push the result.

The order of the pops matters. The right operand was pushed last, so it comes off first; the left operand comes off second. Reversing this turns 3 4 - (which should be -1) into 1.

This is the same machinery that compilers use to evaluate expressions internally and that runtime stack machines use to interpret instructions. A std::stack is enough to implement the core idea in a few lines.

Simulating Recursion with a Stack

Recursion uses the call stack internally: every function call pushes a frame, every return pops one. When recursion goes too deep, the call stack overflows. A common technique to handle deep nesting is to convert recursion into an explicit loop with a std::stack, which lives on the heap (via whatever container backs the stack) and is bounded only by memory, not by the OS-imposed thread stack limit.

Consider walking through a tree of order line items where each item can contain sub-items (bundles). Recursive walks work for small trees; for deeply nested ones, an explicit stack avoids hitting the call-stack limit.

The loop pops the next thing to visit, prints its name, and pushes its children. The order in which children appear depends on push order, but the algorithm itself is "keep going until the stack is empty." Converting any depth-first recursion to this pattern removes the recursion-limit concern.

Choosing a Different Underlying Container

The second template parameter of std::stack is the container it wraps. The default is std::deque<T>, a good general-purpose choice. Any sequence container that supports back(), push_back(), and pop_back() works, which means std::vector<T> or std::list<T>.

The interface is identical regardless of which container backs the stack. The difference is in the performance characteristics of the underlying storage:

Underlying containerMemory layoutPush costIteration locality
std::deque<T> (default)Segmented blocksAmortised O(1), no full reallocationGood, but not contiguous
std::vector<T>One contiguous blockAmortised O(1), can reallocate (copies all elements)Best (single cache-friendly block)
std::list<T>Per-element nodesO(1), never reallocatesWorst (pointers everywhere)

A std::vector-backed stack is the most cache-friendly under heavy push/pop traffic. The catch is that growing the vector occasionally requires reallocating the whole block, and each reallocation is an O(n) copy or move. The deque avoids that by allocating new segments rather than reallocating the whole storage, which is why it is the default.

A std::list-backed stack avoids bulk copying but pays for it with one heap allocation per element and bad cache behaviour. Use it only when iterator stability across pushes is required (which a stack does not expose, so this is rare) or when individual elements are very large and copying them on growth would be expensive.

A std::stack<T, std::vector<T>> has the lowest amortised cost per operation in most workloads because the underlying storage is one contiguous block. The default deque trades a bit of locality for never having to reallocate everything.

For stack-heavy code where this actually matters (a parser, a simulator, a bytecode interpreter), benchmark the alternatives. For an undo log that sees a few dozen pushes per session, the default is fine and the choice does not matter.

When std::stack Is the Wrong Tool

A few cases where std::stack looks like it might fit but is not the right choice:

  • Iteration is required. A stack has no begin/end. To inspect the entire contents, use a std::vector and treat the back as the top directly. The same operations are there: push_back, pop_back, back, plus iteration.
  • Indexed access is required. Same answer. A stack hides random access on purpose; if it is needed, a stack is the wrong type.
  • FIFO (first-in, first-out) order is required. That is std::queue.
  • Multiple ordering at once (priority, then insertion order) is required. That is std::priority_queue.

The advantage of std::stack over a raw vector is that the type itself documents the access pattern. A std::stack<CartSnapshot> in a function signature signals LIFO immediately. A std::vector<CartSnapshot> could be anything.

Quiz

stack Quiz

10 quizzes