Push Back, Push Front, Insert, and Pop in C++ Containers
C++ containers use similar operation names, but the cost behind each name depends on the container. push_back on a std::vector is usually cheap. insert near the beginning of the same vector can be expensive. push_front is natural for std::deque, but it does not even exist for std::vector.
The important question is not only "which function should I call?" It is also "what does the container have to move, allocate, or relink to make this operation work?"
The Short Version
Use std::vector when you mostly append at the end, read by index, and iterate over all elements.
Use std::deque when you need efficient insertion and removal at both ends.
Use std::list when you already have iterators to positions and need stable nodes or frequent splicing.
Use std::forward_list only when singly linked storage is truly useful.
Use std::set, std::map, std::unordered_set, and std::unordered_map when the operation is really about keys, not positions.
push_back: Add at the End
push_back appends one element to the end of a sequence container.
|
1 2 3 4 5 6 7 8 9 10 |
#include <vector> std::vector<int> readings; readings.push_back(120); readings.push_back(121); readings.push_back(118); |
For std::vector, this is amortized constant time. Most calls construct the new element in unused capacity. When the vector runs out of capacity, it allocates a larger block and moves the old elements into it.
That occasional reallocation is why reserve() matters:
|
1 2 3 4 5 6 7 8 9 |
std::vector<int> samples; samples.reserve(4096); for (int value : inputValues) { samples.push_back(value); } |
If the approximate size is known, reserving capacity avoids repeated growth. It also reduces iterator and reference invalidation caused by reallocation.
std::deque also supports efficient push_back, but its storage is split into blocks instead of one contiguous array. It grows well at both ends, although iteration is usually a little less cache friendly than vector iteration.
std::list has constant time push_back, but each element is a node. That means more allocations and more pointer chasing. Big O says the operation is cheap. The CPU cache often disagrees.
push_front: Add at the Beginning
push_front inserts at the front.
|
1 2 3 4 5 6 7 8 9 |
#include <deque> std::deque<int> pendingJobs; pendingJobs.push_front(42); pendingJobs.push_front(17); |
std::deque, std::list, and std::forward_list support this operation efficiently. std::vector does not provide push_front because adding at index zero would require shifting every existing element one step to the right.
You can force front insertion into a vector with insert(begin(), value), but it is usually a sign that the container is wrong:
|
1 2 3 4 5 6 |
std::vector<int> queueLikeData; queueLikeData.insert(queueLikeData.begin(), 99); // linear cost |
For occasional use, that may be fine. For a hot path, prefer std::deque.
pop_back and pop_front: Remove from the Ends
pop_back removes the last element. pop_front removes the first element. Neither function returns the removed value.
|
1 2 3 4 5 6 7 |
if (!readings.empty()) { int last = readings.back(); readings.pop_back(); } |
For std::vector, pop_back is constant time. It destroys the final element and decreases the size. Capacity normally stays unchanged.
std::vector has no pop_front, because removing the first element would shift all remaining elements to the left.
std::deque supports both:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <deque> std::deque<int> fifo; fifo.push_back(10); fifo.push_back(20); int next = fifo.front(); fifo.pop_front(); |
That pattern is why std::deque works well as the default storage for queue like code.
front and back: Look, Do Not Remove
front() returns a reference to the first element. back() returns a reference to the last element. They do not change the container.
|
1 2 3 4 5 6 7 |
if (!fifo.empty()) { int first = fifo.front(); int last = fifo.back(); } |
Always check that the container is not empty before calling them. Calling front() or back() on an empty container is undefined behavior.
std::forward_list has front() but not back(). A singly linked list can find the first node immediately, but finding the last node requires walking through the whole list.
insert: Powerful, but Not Always Cheap
insert places an element at a specified position.
|
1 2 3 4 5 6 7 |
std::vector<int> values {1, 2, 4, 5}; auto pos = values.begin() + 2; values.insert(pos, 3); |
For a vector, inserting in the middle is linear. Elements after the insertion point must move. If the vector needs more capacity, all elements may move to a new allocation.
For a deque, middle insertion is also linear. Deques are optimized for both ends, not for arbitrary middle positions.
For a list, insertion at a known iterator position is constant time:
|
1 2 3 4 5 6 7 8 9 10 11 |
#include <list> std::list<int> numbers {1, 2, 4}; auto place = numbers.begin(); std::advance(place, 2); numbers.insert(place, 3); |
The catch is visible in the example. Finding the position still took a walk through the list. Lists are useful when your algorithm already keeps the iterator it needs.
emplace: Construct in Place
emplace_back, emplace_front, and emplace construct an element directly inside the container.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <string> #include <vector> struct Device { int address; std::string name; }; std::vector<Device> devices; devices.emplace_back(12, "temperature"); |
This can avoid a temporary object. With modern move semantics, the benefit is often small for cheap types, but emplace is still useful for objects with expensive construction or no convenient temporary form.
Do not use emplace automatically. If push_back(Device{12, "temperature"}) is clearer and just as fast, clarity is a valid reason to prefer it.
A Small Benchmark Shape
For container operations, benchmark the exact pattern you care about. A useful benchmark separates end operations from middle operations:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
#include <chrono> #include <deque> #include <iostream> #include <list> #include <vector> template <class Fn> auto measure_ms(Fn action) { auto start = std::chrono::steady_clock::now(); action(); auto stop = std::chrono::steady_clock::now(); return std::chrono::duration_cast<std::chrono::milliseconds>(stop - start).count(); } int main() { constexpr int count = 200000; auto vectorBack = measure_ms([&] { std::vector<int> data; data.reserve(count); for (int i = 0; i < count; ++i) { data.push_back(i); } }); auto dequeFront = measure_ms([&] { std::deque<int> data; for (int i = 0; i < count; ++i) { data.push_front(i); } }); auto listBack = measure_ms([&] { std::list<int> data; for (int i = 0; i < count; ++i) { data.push_back(i); } }); std::cout << "vector push_back: " << vectorBack << " ms\n"; std::cout << "deque push_front: " << dequeFront << " ms\n"; std::cout << "list push_back: " << listBack << " ms\n"; } |
The exact numbers depend on compiler, optimization flags, allocator, CPU, and element type. The trend is usually more important than the raw value.
For int, a reserved vector tends to be very fast because writes are contiguous. A deque is usually strong at front and back operations. A list often loses simple append and iteration tests because every node is separate memory.
A useful benchmark table might look like this after running your own build:
| Operation pattern | Usually strong choice | Why |
|---|---|---|
| Append many small values | std::vector |
Contiguous memory and amortized growth |
| Add and remove at both ends | std::deque |
Efficient front and back operations |
| Insert with saved iterators | std::list |
Relinks nodes without moving elements |
| Lookup by key | std::map or std::unordered_map |
Search structure matches the task |
Iterator and Reference Invalidation
Performance is not the only concern. Operations can invalidate iterators and references.
Vector reallocation invalidates everything that points into the vector. Middle insertion also invalidates iterators and references at or after the insertion point. Deque has its own invalidation rules, especially around insertions. List insertion normally keeps existing iterators valid, because nodes do not move.
This is one reason lists still matter. If an algorithm stores iterators into a container and needs them to remain valid across insertion, node based containers can be simpler.
Practical Rule
Start with std::vector when there is no strong reason to choose something else. It is compact, simple, and fast for iteration.
Switch to std::deque when front operations are part of the normal workload.
Use std::list when stable node identity, splicing, or iterator based insertion is the real requirement.
Avoid choosing a container only by the name of one operation. push_back, push_front, insert, pop_back, and pop_front describe what you want to do. The container decides what that operation costs.