std::forward_list
std::forward_list
, covering creation, management, and advanced list operationsSimilar to arrays like std::vector
and std::array
, a linked list lets us store a sequential collection of objects.
Unlike arrays, linked lists store their objects in a more free-form memory layout, that causes them to have different performance characteristics. We covered these characteristics, and their implications, in more detail in the previous lesson:
std::forward_list
TypeThe C++ standard library provides a ready-made way for us to create and manage linked lists, using the std::forward_list
type.
In this article, we'll explore the basics of the std::forward_list
container and discuss its key features, advantages, and use cases.
There are several different implementations of linked lists, with different capabilities. The most basic form of a linked list is what we described in the previous lesson. In that example, each node in the linked list had a link (or pointer) to the next item in the list.
This is sometimes referred to as a singly linked list, or a forward list, as the list can only be traversed in one direction - forward.
We could expand this concept such that each item in the list also includes a link to the previous item. This would create a doubly linked list, where we can traverse the list in either direction - forward or back.
The C++ standard library also includes an implementation of doubly-linked lists, which it calls std::list
. This lesson focuses on the singly linked list, std::forward_list
, but everything we cover also applies to std::list
.
Forward lists are available by including <forward_list>
#include <forward_list>
We can create a forward list by specifying the type of items it will contain. Below, we create a forward list that stores int
objects:
#include <forward_list>
int main() {
std::forward_list<int> MyList;
}
If we are providing items at the same time we are creating the list, we can omit the type and let the compiler deduce it. This is using class template argument deduction (CTAD):
#include <forward_list>
int main() {
std::forward_list MyList { 1, 2, 3, 4, 5 };
}
Unlike many other containers, std::forward_list
does not keep track of its size - that is, how many objects it currently contains. To determine its size, we need to traverse the entire list and keep track of how many links we had to follow.
The std::distance()
function can do this for us. We pass it the iterators returned from the begin()
and end()
methods respectively:
#include <forward_list>
#include <iostream>
int main() {
std::forward_list MyList { 1, 2, 3, 4, 5 };
std::cout << "Size: " << std::distance(
MyList.begin(), MyList.end());
}
Size: 5
The first element in a linked list can be accessed by calling the front()
method:
#include <forward_list>
#include <iostream>
int main() {
std::forward_list MyList{1, 2, 3, 4, 5};
std::cout << "Front: " << MyList.front();
}
Front: 1
To access elements in arbitrary positions, we need to traverse the links to that position.
We can do this using iterators and std::next()
. Below, we generate an iterator to the third position by traversing two steps from the begin()
iterator:
#include <forward_list>
#include <iostream>
int main() {
std::forward_list MyList{1, 2, 3, 4, 5};
auto Third{std::next(MyList.begin(), 2)};
std::cout << "Third: " << *Third;
}
Third: 3
As we covered in our earlier lessons, linked lists are not designed to support this access pattern. If we require fast access to our objects by index, we should likely be using an array instead of a linked list.
As we covered in the introduction, std::forward_list
is a singly linked list. Each node only includes a link to the next node. As such, it only supports traversal in one direction direction. An implication of this is that we cannot pass a negative offset to std::next()
.
Below, we attempt to access the last element in our std::forward_list
by traversing backward from the end()
iterator:
#include <forward_list>
int main() {
std::forward_list MyList{1, 2, 3, 4, 5};
std::next(MyList.end(), -1);
}
With containers that support bidirectional traversal, this would work. But, std::forward_list
doesn’t. If we compile our project in debug mode, we’ll likely get run-time checks that alert us of the error:
error: negative advance of non-bidirectional iterator
example.exe (process 39308) exited with code 3.
If we require backward traversal, we can use a doubly linked list, such as std::list
.
The std::forward_list
, container supports the range-based for-loop syntax, letting us easily iterate over the entire collection:
#include <iostream>
#include <forward_list>
int main() {
std::forward_list MyList { 1, 2, 3, 4, 5 };
for (int x : MyList) {
std::cout << x << ", ";
}
}
1, 2, 3, 4, 5,
The std::forward_list
provides two methods for adding objects to the start of our list: push_front()
and emplace_front()
The push_front()
method takes an existing object that has already been constructed, and moves or copies it to the front of our linked list:
#include <iostream>
#include <string>
#include <forward_list>
struct Character {/*...*/}
int main() {
std::forward_list<Character> MyList;
Character Wizard { "Roderick" };
MyList.push_front(Wizard);
std::cout << "Front: " << MyList.front().Name;
}
Front: Roderick
The emplace_front()
method constructs a new object in place. The arguments that are provided to emplace_front()
are forwarded to the constructor of that type:
#include <iostream>
#include <string>
#include <forward_list>
struct Character {/*...*/}
int main() {
std::forward_list<Character> MyList;
MyList.emplace_front("Roderick");
std::cout << "Front: " << MyList.front().Name;
}
Front: Roderick
Because emplace_front()
eliminates the need to copy or move an object, it is more performant, so should be preferred over push_front()
where possible.
Given the performance characteristics of forward lists, the most efficient way to add items to the list is at the front. Inserting an object at any other position requires us to traverse the list to find that position.
However, if we’ve already traversed the list and generated an iterator to the position we want to use, we can insert or emplace an object there using insert_after()
or emplace_after()
Similar to our previous section, insert_after()
will move or copy an existing object into our list, whilst emplace_after()
will construct it in place.
insert_after()
The first argument to insert_after()
is an iterator pointing at the node we want our new object to be inserted after.
The second argument is the value to insert. Below, we generate an iterator to the third element using std::next()
, and then insert a new object after it (that is, in the fourth position):
#include <iostream>
#include <forward_list>
int main() {
std::forward_list MyList { 0, 0, 0, 0, 0 };
MyList.insert_after(
std::next(MyList.begin(), 2), 1);
for (int x : MyList) {
std::cout << x << " ";
}
}
0 0 0 1 0 0
emplace_after()
As before, if we can construct our object directly into the linked list, that will be more performant than constructing it outside, and then moving it in.
The emplace_after()
method lets us do this. We pass an iterator as the first argument.
Any additional arguments we provide will be forwarded to an appropriate constructor of the underlying type our list is storing:
#include <iostream>
#include <string>
#include <forward_list>
struct Character {/*...*/}
int main() {
std::forward_list<Character> MyList;
MyList.emplace_front("Roderick", 50);
MyList.emplace_after(
MyList.begin(), "Anna", 45);
for (Character& C : MyList) {
std::cout << std::format(
"{}, Level {}\n", C.Name, C.Level);
}
}
Roderick, Level 50
Anna, Level 45
There are several ways we can remove items from our linked lists:
pop_front()
We can remove the first element in a linked list using pop_front
:
#include <iostream>
#include <forward_list>
int main() {
std::forward_list MyList { 1, 2, 3, 4, 5 };
MyList.pop_front();
for (int x : MyList) {
std::cout << x << ", ";
}
}
2, 3, 4, 5,
remove()
We can remove all elements that are equal to a provided value, using the remove()
method:
#include <iostream>
#include <forward_list>
int main() {
std::forward_list MyList { 1, 2, 3, 4, 5 };
MyList.remove(3);
for (int x : MyList) {
std::cout << x << ", ";
}
}
1, 2, 4, 5,
When using algorithms on containers of a certain type, some of those algorithms may have additional requirements of that type.
For example, the remove()
algorithm removes any objects that pass an equality check. Therefore, the type we’re storing in our container must support that action.
Specifically, the type must support the equality operator, ==
, where the right operand is an object of that same type.
The built-in int
type supports the ==
operator, so our earlier example works as expected.
For more details on the requirements of any algorithm, we should check the documentation. Standard library references are available from many online sources, such as cppreference.com
We introduced how to implement capabilities like ==
to our custom types in our introductory lesson on operator overloading:
remove_if()
The remove_if()
method on the linked list allows us to pass a predicate - a function that receives an object and returns a boolean. The remove_if()
algorithm will pass every object in our collection to our predicate one at a time, and remove any for which our predicate returns true
:
Below, we use this to remove all the negative numbers from our linked list:
#include <iostream>
#include <forward_list>
bool isNegative(int x) { return x < 0; }
int main() {
std::forward_list MyList { -1, 4, -3, 2, 0 };
MyList.remove_if(isNegative);
for (int x : MyList) {
std::cout << x << " ";
}
}
4, 2, 0
Before C++20, remove()
and remove_if()
do not return anything (ie, their return type is void
)
Since C++20, they now return a number representing the number of elements that were removed from the list:
#include <forward_list>
#include <iostream>
bool isNegative(int x) { return x < 0; }
int main() {
std::forward_list MyList{-1, 4, -3, 2, 0};
std::cout << "Removed "
<< MyList.remove_if(isNegative)
<< " Objects";
}
Removed 2 Objects
clear()
The clear()
method simply removes all the objects from the linked list:
#include <forward_list>
#include <iostream>
int main() {
std::forward_list MyList{1, 2, 3, 4, 5};
std::cout << "Size: " << std::distance(
MyList.begin(), MyList.end());
MyList.clear();
std::cout << "\nNew Size: " << std::distance(
MyList.begin(), MyList.end());
}
Size: 5
New Size: 0
erase_after()
Similar to emplace_after()
and insert_after()
, the erase_after()
method interacts with iterators, allowing us to remove elements from anywhere in the list.
Here, we remove the second element, by erasing the element after the begin()
iterator:
#include <iostream>
#include <forward_list>
int main() {
std::forward_list MyList { 1, 2, 3, 4, 5 };
MyList.erase_after(MyList.begin());
for (int x : MyList) {
std::cout << x << ", ";
}
}
1, 3, 4, 5,
Here, we use std::next()
to create an iterator for the 2nd element. We pass it to erase_after()
to remove the third element:
#include <iostream>
#include <iterator>
#include <forward_list>
int main() {
std::forward_list MyList { 1, 2, 3, 4, 5 };
auto Second = std::next(MyList.before_begin(), 2);
MyList.erase_after(Second);
for (int x : MyList) {
std::cout << x << ", ";
}
}
1, 2, 4, 5,
The erase_after()
accepts a second argument. By passing an iterator here, we can specify a range of objects to remove. Objects from the first iterator until the second iterator will be erased, excluding those objects.
Below, we remove everything after the first element until the end of the container, leaving us with only one object. Remember. end()
is a "past-the-end" iterator, meaning the last element of the collection is included in this range:
#include <forward_list>
#include <iostream>
#include <iterator>
int main() {
std::forward_list MyList{1, 2, 3, 4, 5};
auto Start = MyList.begin();
MyList.erase_after(Start, MyList.end());
std::cout << "List Contents: ";
for (int x : MyList) {
std::cout << x << ", ";
}
}
List Contents: 1,
Below, we erase everything from position 2 to position 5, excluding those objects:
#include <forward_list>
#include <iostream>
#include <iterator>
int main() {
std::forward_list MyList{1, 2, 3, 4, 5};
auto Start = std::next(MyList.begin(), 1);
auto End = std::next(Start, 3);
MyList.erase_after(Start, End);
std::cout << "List Contents: ";
for (int x : MyList) {
std::cout << x << ", ";
}
}
List Contents: 1, 2, 5,
std::forward_list
Linked lists can be sorted in place using the sort()
method:
#include <iostream>
#include <forward_list>
int main() {
std::forward_list MyList { 3, 2, 5, 1, 4 };
MyList.sort();
for (int x : MyList) {
std::cout << x << " ";
}
}
1 2 3 4 5
This is another example of an algorithm that has underlying requirements. Specifically, the data type in our list needs to define what it means for their objects to be sorted. This is done by implementing the <
operator.
The sort()
method allows us to pass a function to it, to customize how our objects are sorted. This function will receive two objects from our list and will return true
if the first object should come before the second object.
Using this overload of the sort()
method also means the algorithm no longer needs to use the <
operator, meaning it is compatible with types that don’t implement it.
Here, we sort our Characters by level:
#include <iostream>
#include <forward_list>
struct Character {
std::string Name;
int Level;
};
bool isHigherLevel(
const Character& A, const Character& B) {
return A.Level > B.Level;
}
int main() {
std::forward_list MyList {
Character { "Anna", 20 },
Character { "Roderick", 10 },
Character { "Bruce", 15 },
};
MyList.sort(isHigherLevel);
for (auto x : MyList) {
std::cout
<< x.Level << " - " << x.Name << '\n';
}
}
20 - Anna
15 - Bruce
10 - Roderick
In this lesson, we have explored std::forward_list
, learning how to create, manage, and manipulate singly-linked lists. The key topics we learned included:
std::vector
and std::array
) and linked lists (std::forward_list
), especially in terms of memory layout.<forward_list>
header.std::distance()
to determine the size of a std::forward_list
, as it does not track its size natively.front()
and arbitrary elements using iterators and std::next()
.std::forward_list
in backward traversal and the inability to use negative offsets with std::next()
.std::forward_list
.push_front()
and emplace_front()
.insert_after()
and emplace_after()
.pop_front()
, remove()
, remove_if()
, clear()
, and erase_after()
.std::forward_list
using the sort()
method and customizing the sort behavior with a comparison function.std::forward_list
This lesson provides an in-depth exploration of std::forward_list
, covering creation, management, and advanced list operations
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.