Stack Data Structures using std::stack

An introduction to the stack data structure, and the C++ 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.

3D concept art of an office worker
Ryan McCombe
Ryan McCombe
Posted

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.

  • a queue uses the first in, first out (FIFO) principle
  • a stack uses the last in, first out (LIFO) principle

That is, objects in a stack are accessed in the opposite order to which they were added. In other words, 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.

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;
}

We can populate our stack with initial values by passing an iterator pair to the constructor. We cover iterators in more detail later 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()
  };
}

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)
  };
}

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!

Using a different container with std::stack (Container Adaptors)

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, as long as it supports the following methods:

  • push_back() - adds an object to the top of the stack
  • back() - returns a reference to the object at the top of the stack (i.e., the object that was most recently pushed)
  • pop_back() - returns a reference to the object at the top of the stack (i.e., the object that was most recently pushed)

In other words, the std::stack template simply uses another sequential container and treats the “back” of that container as the “top” of the stack.

If we want our stack to have good performance, ideally the underlying container should also have favorable performance (ideally constant time) in all three of these operations. The default std::deque does meet these 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:

#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

When doing this, we can eliminate the second template parameter, and let the compiler infer it using class template argument deduction (CTAD):

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

int main(){
  std::list Numbers{1, 2, 3};
  std::stack A{Numbers};
}

Was this lesson useful?

Edit History

  • — First Published

Ryan McCombe
Ryan McCombe
Posted
This lesson is part of the course:

Professional C++

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

7a.jpg
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!

This course includes:

  • 106 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Double-Ended Queues using std::deque

A detailed introduction to double-ended queues using std::deque, and how they compare to std::vector
3D character concept art
Contact|Privacy Policy|Terms of Use
Copyright © 2023 - All Rights Reserved