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

Ryan McCombe
Updated

In this lesson, we'll introduce double-ended queues, commonly called deques. As we might expect, a double-ended queue is a queue where we can efficiently manipulate either end, typically called the "back" and the "front" of the queue.

The C++ standard library's implementation of a deque is std::deque, and goes far beyond these basic capabilities. It has a full suite of features, effectively replicating the abilities of dynamic arrays, such as a std::vector.

For example, we can iterate through all the elements of a std::deque, and can directly access elements in any position using the [] operator.

We'll explore all of these capabilities, and see how std::deque compares to other containers throughout this lesson.

Creating a std::deque

The standard library's implementation of a deque is std::deque. available in the <deque> header.

We can create them in a similar way to other containers, by passing the type of data we're storing as a template argument. In this case, we create a std::deque for storing int objects:

#include <deque>

int main(){
  std::deque<int> Deque;
}

We can provide an initial list of values for our std::deque:

#include <deque>

int main(){
  std::deque<int> Deque{1, 2, 3, 4, 5};
}

As we're used to, class template argument deduction (CTAD) is supported. So, we can omit the template argument in scenarios where the compiler can infer it:

#include <deque>

int main(){
  std::deque Deque{1, 2, 3, 4, 5};
}

We can also provide a pair of iterators to specify a series of objects with which to initialize our deque.

We cover iterators in more detail later in the course, but the basic example below shows how we can initialize a deque with every value from another container:

#include <deque>
#include <vector>

int main(){
  std::vector Numbers{1, 2, 3, 4, 5};
  std::deque Deque{
    Numbers.begin(), Numbers.end()
  };
}

As of C++23, we can initialize a std::deque using a range. A range is a recent addition to the language, which we cover in more detail later in the course.

For now, we can note that most of the containers we've been working with so far are ranges, so we can initialize a std::deque using their values like this:

#include <deque>
#include <vector>

int main(){
  std::vector Numbers{1, 2, 3, 4, 5};
  std::deque Deque{std::from_range, Numbers};
}

Iterating over a Deque

The simplest way of iterating over a deque is using a range-based for loop. We cover these in more detail later in the course, but a basic example looks like this:

#include <deque>
#include <iostream>

int main(){
  std::deque Deque{1, 2, 3};

  std::cout << "Values: ";
  for (auto i : Deque) {
    std::cout << i << ", ";
  }
}
Values: 1, 2, 3,

Accessing Front and Back Elements

As we might expect, the main use of a deque focuses on accessing and manipulating elements at either end of the collection.

front()

The front() method returns a reference to the first element in the deque:

#include <deque>
#include <iostream>

int main(){
  std::deque Deque{1, 2, 3};
  std::cout << "Front: " << Deque.front();
}
Front: 1

back()

The back() method returns a reference to the last element in the deque:

#include <deque>
#include <iostream>

int main(){
  std::deque Deque{1, 2, 3};
  std::cout << "Back: " << Deque.back();
}
Back: 3

Adding Objects to the Front and Back

Similar to other containers, std::deque gives us the ability to "emplace" and "push" objects, adding them to the collection. Given the container is double-ended, we can add elements either to the front or the back.

emplace_front()

The most efficient way of adding an object to a deque is by emplacing it. This constructs the object directly into the queue, removing the need to move or copy it into place. Any arguments we pass to an emplace function get forwarded to the constructor for the type our deque is storing.

The emplace_front() function constructs our object at the start of the collection:

#include <deque>
#include <iostream>

class Character {/*...*/}; int main(){ std::deque<Character> Deque; Deque.emplace_front("Roderick"); std::cout << "\nFront: " << Deque.front().Name; }
Constructing Roderick
Front: Roderick

push_front()

When we want to add an object that already exists to our deque, we can use push_back(). This lets us move or copy it into our collection:

#include <deque>
#include <iostream>

class Character {/*...*/}; int main(){ std::deque<Character> Deque; Character Player1{"Roderick"}; Deque.push_front(Player1); std::cout << "\nFront: " << Deque.front().Name; std::cout << "\n\n"; Character Player2{"Anna"}; Deque.push_front(std::move(Player2)); std::cout << "\nFront: " << Deque.front().Name; }
Constructing Roderick
Copying Roderick
Front: Roderick

Constructing Anna
Moving Anna
Front: Anna

We covered the mechanics of moving and copying earlier in our course, in our lessons on memory management:

Copy Semantics and Return Value Optimization

Learn how to control exactly how our objects get copied, and take advantage of copy elision and return value optimization (RVO)

Move Semantics

Learn how we can improve the performance of our types using move constructors, move assignment operators and std::move()

emplace_back()

We can also construct objects into the back of our collection using emplace_back():

#include <deque>
#include <iostream>

class Character {/*...*/}; int main(){ std::deque<Character> Deque; Deque.emplace_back("Roderick"); std::cout << "\nBack: " << Deque.back().Name; }
Constructing Roderick
Back: Roderick

push_back()

And we can push existing objects to the back of our collection using push_back():

#include <deque>
#include <iostream>

class Character {/*...*/}; int main(){ std::deque<Character> Deque; Character Player1{"Roderick"}; Deque.push_back(Player1); std::cout << "\nBack: " << Deque.back().Name; std::cout << "\n\n"; Character Player2{"Anna"}; Deque.push_back(std::move(Player2)); std::cout << "\nBack: " << Deque.back().Name; }
Constructing Roderick
Copying Roderick
Back: Roderick

Constructing Anna
Moving Anna
Back: Anna

Prepending and Appending Ranges

As of C++23, we can now add entire ranges to the front or back of our deque. We cover ranges in more detail later, but most of the sequential containers we've seen so far are valid ranges. This includes deques.

The following examples show how we can push all of the content of such a container into our deque.

append_range()

The append_range() method pushes the contents of a range to the back of our deque:

#include <iostream>
#include <deque>

int main(){
  std::deque Deque{1, 2, 3};
  std::deque ExtraNumbers{4, 5, 6};

  Deque.append_range(ExtraNumbers);

  std::cout << "Values: ";
  for (auto i : Deque) {
    std::cout << i << ", ";
  }
}
Values: 1, 2, 3, 4, 5, 6,

prepend_range()

The prepend_range() method pushes the contents of a range to the front of our deque:

#include <iostream>
#include <deque>

int main(){
  std::deque Deque{1, 2, 3};
  std::deque ExtraNumbers{-2, -1, 0};

  Deque.prepend_range(ExtraNumbers);

  std::cout << "Values: ";
  for (auto i : Deque) {
    std::cout << i << ", ";
  }
}
Values: -2, -1, 0, 1, 2, 3,

Removing the First and Last Elements

Similar to regular queues, we can "pop" objects, removing them. Naturally, with double-ended queues, we can pop elements from either end.

pop_front()

The pop_front() method removes the element that is first in the queue:

#include <iostream>
#include <deque>

int main(){
  std::deque Deque{1, 2, 3};

  std::cout << "Front: " << Deque.front();
  Deque.pop_front();
  std::cout << "\nFront: " << Deque.front();
  Deque.pop_front();
  std::cout << "\nFront: " << Deque.front();
}
Front: 1
Front: 2
Front: 3

pop_back()

The pop_back() method removes the element that is last in the queue:

#include <iostream>
#include <deque>

int main(){
  std::deque Deque{1, 2, 3};

  std::cout << "Back: " << Deque.back();
  Deque.pop_back();
  std::cout << "\nBack: " << Deque.back();
  Deque.pop_back();
  std::cout << "\nBack: " << Deque.back();
}
Back: 3
Back: 2
Back: 1

Deque Size

There are two main ways we can check how many elements are in our deque

size()

The size() method returns a size_t, representing how many objects are in the collection:

#include <iostream>
#include <deque>

int main(){
  std::deque Deque{1, 2, 3};

  std::cout << "Size: " << Deque.size();
  Deque.pop_front();
  std::cout << "\nSize: " << Deque.size();
  Deque.emplace_front(1);
  std::cout << "\nSize: " << Deque.size();
}
Size: 3
Size: 2
Size: 3

empty()

If we want to know whether the collection is empty - that is, if its size() == 0 we can use the more descriptive empty() method:

#include <iostream>
#include <deque>

int main(){
  std::deque Deque{1};

  if (!Deque.empty()) {
    std::cout << "The deque is not empty\n";
  }

  Deque.pop_front();

  if (Deque.empty()) {
    std::cout << "But now it is";
  }
}
The deque is not empty
But now it is

Accessing Arbitrary Elements

Similar to arrays like std::array and std::vector, the std::deque container provides access to arbitrary elements in any position.

The [] operator

We can get a reference to an element in any position by passing the position index to the [] operator. As usual, the indexing is zero-based, that is, the element in the first position is at index 0:

Therefore, [0] is equivalent to accessing the front() element, and [1] accesses the second element. The last element, equivalent to the back() method is at index size() - 1:

#include <iostream>
#include <deque>

int main(){
  std::deque Deque{1, 2, 3};

  // Equivalent to Deque.front()
  std::cout << "First: " << Deque[0];

  std::cout << "\nSecond: " << Deque[1];

  // Equivalent to Deque.back()
  std::cout
    << "\nLast: "
    << Deque[Deque.size() - 1];
}
First: 1
Second: 2
Last: 3

The at() method

The at() method also gives us access to arbitrary elements, by passing its index as an argument. The at() method performs an additional check to ensure the index we pass is within a valid range, given the size of our deque.

#include <iostream>
#include <deque>

int main(){
  std::deque Deque{1, 2, 3};

  std::cout << "First: " << Deque.at(0);
  std::cout << "\nSecond: " << Deque.at(1);
  std::cout << "\nThird: " << Deque.at(2);
}
First: 1
Second: 2
Third: 3

The key difference between [] and at() is that If the index passed to at() is not within the bounds of our collection, an exception will be created.

We cover exceptions in more detail later in the course - for now, we can just note that they're a way we can detect and handle error states in our programs:

#include <iostream>
#include <deque>

int main(){
  std::deque Deque{1, 2, 3};

  try { Deque.at(100); }
  catch (...) {
    std::cout << "Something went wrong...\n";
  }
  std::cout << "but we caught it and recovered";
}
Something went wrong...
but we caught it and recovered

Erasing Arbitrary Elements

We can erase one or more elements from our collection using the erase() method.

Erasing a Single Element

The erase() method requires an iterator that points to the element in the deque to erase. We cover iterators in detail later in the course, but we can effectively get an iterator to the index we want by using the begin() method, alongside the + operator to get to any index we want.

The following example creates iterators for index 0, 1, and 2:

#include <deque>
#include <iostream>

int main(){
  std::deque Deque{1, 2, 3};

  auto Iterator0 { Deque.begin() };
  auto Iterator1 { Deque.begin() + 1 };
  auto Iterator2 { Deque.begin() + 2 };
}

In this example, we apply this technique to erase the element at index 2 of our deque:

#include <deque>
#include <iostream>

int main(){
  std::deque Deque{1, 2, 3, 4, 5};

  Deque.erase(Deque.begin() + 2);

  std::cout << "Values: ";
  for (auto i : Deque) {
    std::cout << i << ", ";
  }
}
Values: 1, 2, 4, 5,

Erasing Multiple Elements

An alternative form of the erase() method accepts a pair of iterators. Everything from the first iterator to the second iterator will be erased. The following example erases the middle three elements of our collection:

#include <deque>
#include <iostream>

int main(){
  std::deque Deque{1, 2, 3, 4, 5};

  Deque.erase(Deque.begin() + 1,
              Deque.end() - 1);

  std::cout << "Values: ";
  for (auto i : Deque) {
    std::cout << i << ", ";
  }
}
Values: 1, 5,

Inserting Elements in any Position

We are not restricted to inserting elements just at the beginning or end of a deque. We have several options for inserting elements into any part of the deque.

emplace()

The emplace() method constructs objects in place. Where this is possible, it is the preferred method, as it minimizes the performance overhead of moving or copying objects.

The first argument to emplace() is an iterator denoting where we want the object to be constructed. Any remaining arguments are forwarded to the constructor of the type our deque is storing.

#include <deque>
#include <iostream>

int main(){
  std::deque Deque{100, 300};

  Deque.emplace(Deque.begin() + 1, 200);

  std::cout << "Values: ";
  for (int x : Deque) {
    std::cout << x << ", ";
  }
}
Values: 100, 200, 300,

Below, we have a more complex example, where we're constructing an object of a custom type:

#include <deque>
#include <iostream>

class Character {/*...*/}; int main(){ std::deque<Character> Deque; Deque.emplace_back("Player 1"); Deque.emplace_back("Player 3"); Deque.emplace(Deque.begin() + 1, "Player 2"); std::cout << "Values: "; for (const Character& C : Deque) { std::cout << C.Name << ", "; } }
Values: Player 1, Player 2, Player 3,

insert() a single object

When we want to add an object that already exists to our deque, we can use the insert() method. The first argument will be an iterator pointing to where we want the object to be inserted. The second argument will be the object we want to copy or move.

In the following example, we use the insert() method to copy one object into our deque, and to move another.

#include <deque>
#include <iostream>

class Character {/*...*/}; int main(){ std::deque<Character> Deque; Deque.emplace_back("Player 1"); Deque.emplace_back("Player 4"); // Copying a Character into the deque Character Player2{"Player 2"}; Deque.insert(Deque.begin() + 1, Player2); // Moving a Character into the deque Character Player3{"Player 3"}; Deque.insert(Deque.begin() + 2, std::move(Player3)); std::cout << "Values:\n"; for (const Character& C : Deque) { std::cout << C.Name << '\n'; } }
Values:
Player 1
Player 2
Player 3
Player 4

insert() multiple copies of an object

Another form of the insert() method allows us to insert multiple copies of an object. It uses three arguments:

  1. An iterator to the position we want to insert the object
  2. An integer representing the number of copies we want to insert
  3. The object we want to copy

In the following example, we insert 2 copies of the 10 object, starting from index 1 of our deque:

#include <deque>
#include <iostream>

int main(){
  std::deque Deque{1, 2, 3};

  Deque.insert(Deque.begin() + 1, 2, 10);

  std::cout << "Values: ";
  for (int x : Deque) {
    std::cout << x << ", ";
  }
}
Values: 1, 10, 10, 2, 3,

insert() multiple objects

The insert() method also allows us to pass two additional iterators as the second and third arguments. Objects from the second iterator to the third iterator will be inserted into our deque.

Below, we select the middle three elements of our Extra collection, and insert them into our Deque collection, starting at index 1:

#include <deque>
#include <iostream>

int main(){
  std::deque Deque{1, 5, 6};
  std::deque Extra{1, 2, 3, 4, 5};

  Deque.insert(Deque.begin() + 1,
               Extra.begin() + 1,
               Extra.end() - 1);

  std::cout << "Values: ";
  for (int x : Deque) {
    std::cout << x << ", ";
  }
}
Values: 1, 2, 3, 4, 5, 6,

insert_range()

As of C++23, we can insert a range into our deque, using the insert_range() method. The first argument is an iterator denoting where we want the range to be inserted, and the second argument is the range we want to insert.

#include <deque>
#include <iostream>

int main(){
  std::deque Deque{1, 5, 6};
  std::deque Extra{2, 3, 4};

  Deque.insert_range(Deque.begin() + 1, Extra);

  std::cout << "Values: ";
  for (int x : Deque) {
    std::cout << x << ", ";
  }
}
Values: 1, 2, 3, 4, 5, 6,

Replacing Contents

We have a few options that let us replace the entire contents of our deque with a new set of objects.

assign()

We can replace the contents in our std::deque by passing a list of new objects to the assign() method:

#include <deque>
#include <iostream>

int main(){
  std::deque Deque{1, 2, 3};

  Deque.assign({4, 5, 6});

  std::cout << "Values: ";
  for (int x : Deque) {
    std::cout << x << ", ";
  }
}
Values: 4, 5, 6,

An alternative form of the assign() method accepts two arguments - a quantity and an object. Our deque will then be updated to contain that quantity of copies of the object. In the following example, we set our deque to contain 5 copies of the 2 object:

#include <deque>
#include <iostream>

int main(){
  std::deque Deque{1, 2, 3};

  Deque.assign(5, 2);

  std::cout << "Values: ";
  for (int x : Deque) {
    std::cout << x << ", ";
  }
}
Values: 2, 2, 2, 2, 2,

The assign method can also receive a pair of iterators, replacing the deque contents with everything from the first iterator until the second iterator.

Below, we update our Deque to contain the middle 3 objects from the Replacements collection:

#include <deque>
#include <iostream>

int main(){
  std::deque Deque{1, 2, 3};
  std::deque Replacements{1, 2, 3, 4, 5};

  Deque.assign(Replacements.begin() + 1,
               Replacements.end() - 1);

  std::cout << "Values: ";
  for (int x : Deque) {
    std::cout << x << ", ";
  }
}
Values: 2, 3, 4,

assign_range()

As of C++23, we can replace the contents of a deque with the contents of any range, using the assign_range() method:

#include <deque>
#include <iostream>

int main(){
  std::deque Deque{1, 2, 3};
  std::deque Replacements{4, 5, 6};

  Deque.assign_range(Replacements);

  std::cout << "Values: ";
  for (int x : Deque) {
    std::cout << x << ", ";
  }
}
Values: 4, 5, 6,

clear()

Everything in our deque can be removed using the clear() method:

#include <deque>
#include <iostream>

int main(){
  std::deque Deque{1, 2, 3};

  Deque.clear();

  if (Deque.empty()) {
    std::cout << "The deque is empty";
  }
}
The deque is empty

swap()

The swap() method switches the content within two deque objects. This is highly performant, and one of the most efficient ways of moving a std::deque.

#include <deque>
#include <iostream>

int main(){
  std::deque A{1, 2, 3};
  std::deque B{4, 5, 6};

  A.swap(B);

  std::cout << "A Values: ";
  for (int x : A) { std::cout << x << ", "; }

  std::cout << "\nB Values: ";
  for (int x : B) { std::cout << x << ", "; }
}
A Values: 4, 5, 6,
B Values: 1, 2, 3,

Memory Management

Similar to std::vector, the std::deque container includes some additional methods to provide some high-level memory management.

resize()

The resize() method changes the number of objects our deque is holding. If we resize() the container to be smaller than its current size, we are effectively pruning elements from the end of the container:

#include <deque>
#include <iostream>

int main(){
  std::deque Deque{1, 2, 3};

  std::cout << "Size: " << Deque.size();
  Deque.resize(1); 
  std::cout << "\nNew Size: " << Deque.size();
  std::cout << "\nValue: " << Deque.front();
}
Size: 3
New Size: 1
Value: 1

If we're resizing the deque to be bigger than its current size, the excess space will be filled with default-constructed objects of the type our deque is holding.

We can customize this by passing a second argument to the resize() function, passing a different value to be used instead.

Below, we resize our deque from 3 to 5, and use the value 10 to fill the additional two spaces:

#include <deque>
#include <iostream>

int main(){
  std::deque Deque{1, 2, 3};

  std::cout << "Size: " << Deque.size();

  Deque.resize(5, 10); 

  std::cout << "\nNew Size: " << Deque.size();

  std::cout << "\nValues: ";
  for (int x : Deque) {
    std::cout << x << ", ";
  }
}
Size: 3
New Size: 5
Values: 1, 2, 3, 10, 10,

max_size()

The max_size() method returns a size_t representing the maximum number of objects our std::deque can hold in the current environment.

In most scenarios, this is likely to be a very big number and not something we need to worry about. But, in some environments or use cases, it's worth remembering that constraints like this do exist.

#include <iostream>
#include <deque>

int main(){
  std::deque Deque{1, 2, 3};
  std::cout << "Max Size: " << Deque.max_size();
}
Max Size: 4611686018427387903

shrink_to_fit()

Finally, we have the shrink_to_fit() method. This requests that the std::deque free any memory it is not using.

In general, this is almost never required. Behind the scenes, deques are much more proactive about managing their memory than other containers such as std::vector. As such, they rarely get into a state where they're holding significantly more memory than they need.

But, in memory-constrained environments, we still have the option to manually intervene if needed:

#include <deque>

int main(){
  std::deque<int> Deque;
  for (int i{0}; i < 1'000; ++i) {
    Deque.push_back(i);
  }

  for (int i{0}; i < 900; ++i) {
    Deque.pop_back();
  }

  Deque.shrink_to_fit();
}

Summary

In this lesson, we've explored the std::deque container in C++, which provides a double-ended queue. We've seen how to create deques, manipulate elements at both ends, and access elements at arbitrary positions. Key takeaways:

  • std::deque is a container adapter that provides efficient insertion and deletion at both ends
  • Elements in a std::deque are not stored contiguously in memory
  • Elements can be inserted at the front using push_front() and emplace_front(), and at the back using push_back() and emplace_back()
  • Entire ranges can be inserted at the front using prepend_range() and at the back using append_range()
  • Elements can be removed from the front using pop_front() and from the back using pop_back()
  • The size of a std::deque can be checked using size() and empty()
  • Random access to elements is provided through the [] operator and the at() method
  • Elements can be erased at arbitrary positions using erase()
  • Elements can be inserted at arbitrary positions using insert() and emplace()
  • The contents of a std::deque can be replaced using assign(), assign_range(), or clear()
  • The size of a std::deque can be changed using resize(), and its capacity can be optimized using shrink_to_fit()
  • The maximum size of a std::deque is given by max_size(), which depends on the system's memory constraints
Next Lesson
Lesson 63 of 128

First Class Functions

Learn about first-class functions in C++: a feature that lets you store functions in variables, pass them to other functions, and return them, opening up new design possibilities

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Performance differences between std::vector and std::deque
When should I use a std::deque instead of a std::vector for optimal performance?
How std::deque is implemented internally
How does a std::deque store its elements in memory?
Exception safety of std::deque operations
Which std::deque operations are exception-safe?
Benefits of using emplace over insert
What are the advantages of using emplace_front() or emplace_back() instead of push_front() or push_back()?
Iterator invalidation rules for std::deque
When do std::deque operations invalidate iterators?
Thread safety of std::deque
Is std::deque thread-safe? Can multiple threads access a deque concurrently?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant