Introduction to Stacks using std::stack

An introduction to the stack data structure, and the standard library implementation - std::stack
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Illustration representing computer hardware
Ryan McCombe
Ryan McCombe
Updated

A stack is another fundamental data structure frequently used to solve programming problems. Similar to a queue, they are used when we need a container that constrains the order in which its objects are accessed.

The key difference is that a stack uses the opposite principle. Whilst a queue constrains access based on a first in, first out (FIFO) principle, a stack is last in, first out (LIFO)

That is, objects in a stack are accessed in the opposite order they were added. The most recently added object is accessed first.

Stacks are often visualized as a vertical tower of objects, where the most recently added items are at the top. As such, only the topmost object is accessible:

Diagram illustrating a stack

Objects deeper in the stack only become accessible when the objects above them are removed. Removing an object from the top of the stack is sometimes referred to as "popping" it. Once we pop an object from the stack, the object that was immediately below it will then become the stack’s top object.

Diagram illustrating an object being popped from a stack

When we add objects to a stack, this is often referred to as "pushing" them. Objects are always pushed to the top of the stack:

Diagram illustrating an object being pushed onto a stack

Stacks are a ubiquitous technique for solving problems we’re likely to encounter when programming complex software. Some common examples include:

1. Code Compilation

The most obvious use of stacks is one we’ve already been using this whole time. We saw how the execution of our program is a function call stack.

Code compilers are themselves written in code and, as we might expect, function call stacks are examples of stacks in action.

It’s not the only example - stacks are also ubiquitous in the evaluation of expressions that appear in our source code.

2. Backtracking Algorithms

Almost any form of backtracking involves a stack under the hood. Examples of this include "undo" functionality in programs, where actions need to be reverted in the opposite order in which they were performed. That is, last in, first out.

The back button within browsers is another example of this principle that we use every day.

3. Graph Algorithms

Stacks are also fundamental building blocks that power a lot of useful algorithms, particularly graph algorithms. Graphs are more complex data structures than what we’ve seen so far. Within a graph, our objects can be connected to any other object in the collection. The main use case for these data structures is to model real-world networks, such as road systems and the Internet.

Stacks underpin the most common graph algorithm - depth-first search. This is used to navigate through the graph, powering capabilities such as navigation within a GPS system, and enabling AI characters to move through virtual worlds within games.

Creating a std::stack

The standard library’s implementation of a stack is std::stack, and is available within the <stack> header.

std::stack objects can be constructed in the normal way - we just need to pass a template parameter denoting the type of objects our stack will contain. In this case, we create a stack to hold int objects:

#include <stack>

int main(){
  std::stack<int> Numbers;
}

Prepopulating a Stack using Iterators

We can populate our stack with initial values by passing an iterator pair to the constructor. We covered iterators in more detail earlier in the course:

In the following code, we give a simple example of creating a stack using all the objects from a std::vector

In this case, our stack will contain the numbers 1, 2, and 3, with 3 being at the "top" of the stack:

#include <stack>
#include <vector>

int main(){
  std::vector Source{1, 2, 3};
  std::stack<int> Numbers{
    Source.begin(), Source.end()
  };
}

Copying and Moving Stacks

We can also create stacks by copying and moving other stacks:

#include <stack>
#include <vector>

int main(){
  std::stack<int> Numbers;
  std::stack<int> NumbersCopy{Numbers};
  std::stack<int> NumbersMove{
    std::move(Numbers)
  };
}

Class Template Argument Deduction

As always, we can use Class Template Argument Deduction (CTAD) to remove template arguments in many cases. The compiler correctly deduces that all of the containers we’re constructing in this example will be holding int objects:

#include <stack>
#include <vector>

int main(){
  std::vector Source{1, 2, 3};
  std::stack Numbers{
    Source.begin(), Source.end()
  };
  std::stack NumbersCopy{Numbers};
}

We can also construct a queue directly from a specific type of container, which we cover later in this lesson.

Accessing the Top Element

We can get a reference to the top element of our stack using the top() method:

#include <stack>
#include <vector>
#include <iostream>

int main(){
  std::vector Numbers{1, 2, 3};
  std::stack Stack{
    Numbers.begin(), Numbers.end()
  };

  std::cout << "Top: " << Stack.top();
}
Top: 3

Removing the Top Element

The pop() method allows us to remove the top element from the stack. This reduces the size (height) of our stack, and the element that was previously second from the top will now be the top element:

#include <stack>
#include <vector>
#include <iostream>

int main(){
  std::vector Numbers{1, 2, 3};
  std::stack Stack{
    Numbers.begin(), Numbers.end()
  };

  std::cout << "Top: " << Stack.top();
  Stack.pop();
  std::cout << "\nTop: " << Stack.top();
}
Top: 3
Top: 2

Adding Objects to the Stack

There are three main methods we use to add items to the stack. Those are emplace(), push() and, since C++23, push_range()

emplace()

Similar to other containers, the most performant way of adding objects to a stack is to construct them directly in the stack. This is sometimes referred to as constructing the object in place.

To do this, we use the emplace() method. Any arguments we provide to emplace() will be forwarded to the constructor of the type our stack contains.

#include <stack>
#include <iostream>

class Character {/*...*/}; int main(){ std::stack<Character> Stack; Stack.emplace("Roderick"); std::cout << "\nTop: " << Stack.top().Name; }
Constructing Roderick
Top: Roderick

push()

When the object we want to add to the stack already exists, we can use the push() method to copy or move it into our stack. Below, we push two objects onto our stack - one by copying an existing object, and one by moving that same object:

#include <stack>
#include <iostream>

class Character {/*...*/}; int main(){ std::stack<Character> Stack; Character Roderick{"Roderick"}; Stack.push(Roderick); Stack.push(std::move(Roderick)); }
Constructing Roderick
Copying Roderick
Moving Roderick

We cover copy semantics, move semantics, and std::move in more detail here:

push_range()

As of C++23, std::stack has gained the push_range() method, which allows us to add multiple objects to the stack at once.

We cover ranges in more detail later in the course, but for now, we can note that most of the sequential containers we’ve seen so far are examples of a range.

Below, we push all the contents of a std::vector to our stack:

#include <stack>
#include <iostream>
#include <vector>

int main(){
  std::vector Numbers{3, 4, 5};
  std::stack<int> Stack;

  Stack.emplace(1);
  Stack.emplace(2);
  Stack.push_range(Numbers);

  std::cout << "Top: " << Stack.top();
}
Top: 5

Getting the Stack Size

There are two main ways we can check how many objects are in our std::stack - the size() method and the empty() method.

size()

The size() method returns a size_t - an integer that tells us how many objects are in our std::stack:

#include <stack>
#include <iostream>

int main(){
  std::stack<int> Stack;

  Stack.emplace(0);
  Stack.emplace(0);
  Stack.emplace(0);

  std::cout << "Stack Size: " << Stack.size();
  Stack.pop();
  std::cout << "\nStack Size: " << Stack.size();
}
Stack Size: 3
Stack Size: 2

empty()

In many cases, our motivation for calling the size() method is to check if there are any objects in the stack at all. That is, if size() == 0.

The empty() method gives us a more descriptive way to implement that check:

#include <stack>
#include <iostream>

int main(){
  std::stack<int> Stack;

  Stack.emplace(0);

  if (!Stack.empty()) {
    std::cout << "The stack is not empty";
  }

  Stack.pop();

  if (Stack.empty()) {
    std::cout << "\nNow it is empty";
  }
}
The stack is not empty
Now it is empty

Typical Stack Usage

The following example shows a basic, typical recipe for how stacks are used. which combines the top(), pop() and empty() methods.

We have a loop that continues as long as the stack has objects - ie, it is not empty().

The top element is then accessed using the top() method, and handled in whatever way our use case requires. Once processing is complete, it is removed from the stack using the top() method, and the next iteration of our loop continues.

#include <stack>
#include <iostream>
#include <vector>

void Process(int Number){
  std::cout << "Processing " << Number << '\n';
}

int main(){
  std::vector Nums{1, 2, 3};
  std::stack Stack{Nums.begin(), Nums.end()};

  while (!Stack.empty()) {
    Process(Stack.top());
    Stack.pop();
  }

  std::cout << "Done!";
}
Processing 3
Processing 2
Processing 1
Done!

std::stack is a Container Adaptor

Similar to std::queue, the std::stack template is an example of a container adaptor.

Container adaptors do not directly manage the storage of the objects they contain. They defer that to some other container, and then simply provide an interface that makes that container behave in the desired way.

By default, std::stack uses a std::deque - a double-ended queue - as the underlying container. We cover std::deque in detail later in this chapter.

The underlying container can be changed by passing a second template argument when creating our std::stack. In this example, we use a std::list instead:

#include <stack>
#include <list>

int main(){
  std::stack<int, std::list<int>> Stack;
}

A std::stack can use any sequential container - it treats the "back" of that container as the "top" of the stack. Specifically, the container needs to provide the following methods:

  • push_back() - adds an object to the top of the stack. Used by the push() method of std::stack
  • emplace_back() - constructs an object at the top of the stack. Used by the emplace() method of std::stack
  • back() - returns a reference to the object at the top of the stack. Used by the top() method of std::stack
  • pop_back() - removes the object at the top of the stack. Used by the pop() method of std::stack

If we want our stack to have high performance, the underlying container should also have favorable performance (ideally constant time) in all three of these operations. The default std::deque meets this criteria so unless there’s a compelling reason to use something else, the default option is generally good.

Initializing a Stack from an Underlying Container

When our objects are already in a container that is a viable candidate to underly a std::stack, we can create it directly from that container. The "back" of that container will become the "top" of the stack.

Below, we use a doubly linked list, std::list, as the basis for our stack, and we prepopulate it by copying an existing std::list:

#include <stack>
#include <list>
#include <iostream>

int main(){
  std::list Numbers{1, 2, 3};
  std::stack<int, std::list<int>> A{Numbers};
  std::cout << "Top Object: " << A.top();
}
Top Object: 3

If preferred, we can alternatively move the list rather than copying it. We can also omit the template arguments and use CTAD if we wish:

#include <stack>
#include <list>
#include <iostream>

int main(){
  std::list Numbers{1, 2, 3};
  std::stack A{std::move(Numbers)};
  std::cout << "Top Object: " << A.top();
}
Top Object: 3

Summary

This lesson provides an in-depth overview of the stack data structure and how to use it in C++ with the std::stack container adaptor. Key takeaways include:

  • Stacks are a last-in, first-out (LIFO) data structure, where elements are accessed in the reverse order they were added
  • Stacks have many applications, including code compilation, backtracking algorithms, and graph algorithms like depth-first search
  • std::stack¬†is the standard library implementation of a stack, available in the¬†<stack>¬†header
  • Stacks can be constructed by passing iterators, copying/moving other stacks, or using class template argument deduction (CTAD)
  • The top element can be accessed with¬†top()¬†and removed with¬†pop()
  • Elements can be added to the stack with¬†emplace(),¬†push(), and in C++23,¬†push_range()
  • The size of a stack can be checked with¬†size()¬†or¬†empty()
  • std::stack¬†is a container adaptor that uses¬†std::deque¬†by default, but can use other sequential containers
  • Stacks can be initialized directly from an underlying container

Was this lesson useful?

Next Lesson

Double-Ended Queues using std::deque

A guide to double-ended queues - a structure that behaves like a vector, specialised for manipulating objects at the edges of the collection
Illustration representing computer hardware
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Standard Library Data Structures
Next Lesson

Double-Ended Queues using std::deque

A guide to double-ended queues - a structure that behaves like a vector, specialised for manipulating objects at the edges of the collection
Illustration representing computer hardware
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved