TODO: check and update this in all readmes
cmake --build build --target ch18_first
./build/ch18_firstBy the end of this chapter, you’ll be able to:
Sorting an array is the process of arranging elements of an array in a specific order (e.g., ascending or descending). There are many cases in which sorting an array is useful, for example:
- Mail servers sort emails by date and time to make it easier to find specific emails.
- Phone books sort names alphabetically to make it easier to find specific names.
Sorting an array can make searching an array more efficient, for both humans and computers. This is because you can apply efficient search algorithm that can be applied to a sorted array only (e.g., binary search, that has a time complexity of O(log n)). This is very useful in cases where the array length is large (e.g., phone books, mail servers).
Additionally, imagine that you need to look if a name is present in a phone book. If the phone book is sorted alphabetically, we will only need to search up to the point where we encounter a name that is greater (alphabetically) than the name we are looking for. This is because the phone book is sorted alphabetically, and we know that all names that come after the name we are looking for will also be greater than the name we are looking for.
Sorte arrays are great, but it has some downsides: sorting an array is expensive as well! In many cases, it is not worth sorting an array to make searching more efficient, unless you are going to search the array many many times.
In some other cases, sorting an array makes searching unnecessary. Imagine the case where we want to find the best test score in a list of test scores. If the list is sorted in descending order, we can simply return the first element of the list (O(1)). In case the list is not sorted, we will need to search the list to find the best test score (O(n)).
Sorting is generally performed by repeatedly comparing pairs of elements and swapping them if they meet some predefined criteria. The order in which these elements are compared depends on the sorting algorithm is used. The criteria depends on how the list will be sorted (e.g., ascending or descending).
In order to swap elements, we could use std::swap function from the C++ standard library, which is defined in the <utility> header:
#include <iostream>
#include <utility>
int main()
{
int x{ 2 };
int y{ 4 };
std::cout << "Before swap: x = " << x << ", y = " << y << '\n';
std::swap(x, y); // swap the values of x and y
std::cout << "After swap: x = " << x << ", y = " << y << '\n';
return 0;
}This program prints:
Before swap: x = 2, y = 4
After swap: x = 4, y = 2
This function performs a swap of two elements, allowing the user to not use a temporary variable to perform the swap (multiple lines of code).
Many sorting algoithms exist, and each of them has its own characteristics. Some most common sorting algorithms are:
- Selection sort: repeatedly find the minimum element from the unsorted part of the array and swap it with the first unsorted element (O(n^2)).
- Insertion sort: repeatedly insert the current element in the sorted part of the array (O(n^2)).
- Bubble sort: repeatedly swap adjacent elements if they are in the wrong order (O(n^2)).
- Quick sort: repeatedly partition the array into two parts and sort them (O(n log n)).
- Merge sort: repeatedly merge two sorted arrays (O(n log n)).
- Heap sort: repeatedly remove the maximum element from the heap (O(n log n)).
Among these algorithms, proably the easiest one to understand and implement is selection sort. This algorithm performs the following steps to sort an array in ascending order:
- Starting at array index 0 (
i = 0), search the entire array (from indexi = 0to array size) to find the minimum element. - Swap the minimum element with the element at index
i = 0. - Repeat steps 1 and 2 for the subarray starting at the next index
i(i++), until the array is sorted (i.e.,i < array size).
In other words, in the first iteration, we are going to find the smallest element in the array, and swap it into the first position. In the second iteration, we are going to find the smallest element in the subarray (excluding the first element), and swap it into the second position. And so on, until the array is sorted.
Here is an example of this algorithm in action. Let' start with a simple array:
{ 30, 50, 20, 10, 40 }
First, we are going to find the smallest element, starting from index 0 (i.e., i = 0), that is 10.
We then swap the smallest element with the element at index 0, that is 30, resulting in an array:
{ 10, 50, 20, 30, 40 }
Now, the first element is sorted, and the unsorted subarray starts at index 1 (i.e., i = 1), and it is { 50, 20, 30, 40 }. We repeat the process, finding the smallest element in the unsorted subarray, that is 20, and swapping it with the element at index 1, that is 50, resulting in an array:
{ 10, 20, 50, 30, 40 }
Now, the first two elements are sorted, and the unsorted subarray starts at index 2 (i.e., i = 2), and it is { 50, 30, 40 }. The smallest among these elements is 30, and we swap it with the element at index 2, that is 50, resulting in the array:
{ 10, 20, 30, 50, 40 }
Now, the first three elements are sorted, and the unsorted subarray starts at index 3 (i.e., i = 3), and it is { 50, 40 }. The smallest among these elements is 40, and we swap it with the element at index 3, that is 50, resulting in the array:
{ 10, 20, 30, 40, 50 }
The last step is not necessary, since the array is already sorted, and the unsorted subarray is just a single element. The algorithm is now complete.
Here is how selection sort algorithm is implemented in C++:
#include <iostream>
#include <iterator>
#include <utility>
int main()
{
int array[]{ 30, 50, 20, 10, 40 };
constexpr int length{ static_cast<int>(std::size(array)) };
// Step through each element of the array
// (except the last one, which will already be sorted by the time we get there)
for (int startIndex{ 0 }; startIndex < length - 1; ++startIndex)
{
// smallestIndex is the index of the smallest element we’ve encountered this iteration
// Start by assuming the smallest element is the first element of this iteration
int smallestIndex{ startIndex };
// Then look for a smaller element in the rest of the array
for (int currentIndex{ startIndex + 1 }; currentIndex < length; ++currentIndex)
{
// If we've found an element that is smaller than our previously found smallest
if (array[currentIndex] < array[smallestIndex])
// then keep track of it
smallestIndex = currentIndex;
}
// smallestIndex is now the index of the smallest element in the remaining array
// swap our start element with our smallest element (this sorts it into the correct place)
std::swap(array[startIndex], array[smallestIndex]);
}
// Now that the whole array is sorted, print our sorted array as proof it works
for (int index{ 0 }; index < length; ++index)
std::cout << array[index] << ' ';
std::cout << '\n';
return 0;
}Here you can see that:
- The outer loop goes from element in position
0tolength - 2(i.e., the second last element), since the last element will already be sorted by the time we get there. - At each iteration of the outer loop, we are going to find the smallest element (at
smallestIndex) in the unsorted subarray (i.e., the subarray starting atstartIndex), and swap it with the element at the current position of the outer loop (i.e.,startIndex). This nested loop is just used to find the smallest element in the unsorted subarray.
Because sorting arrays is so common, the C++ standard library includes a function called std::sort (that lives in ` header), that can be invoked on array as:
#include <algorithm> // for std::sort
#include <iostream>
#include <iterator> // for std::size
int main()
{
int array[]{ 30, 50, 20, 10, 40 };
std::sort(std::begin(array), std::end(array));
for (int i{ 0 }; i < static_cast<int>(std::size(array)); ++i)
std::cout << array[i] << ' ';
std::cout << '\n';
return 0;
}By default, std::sort sorts in non-descending order (i.e., ascending order), using the operator< to compare pairs of elements and swap them if they are in the wrong order.
- Sorting is a very common operation, that can help efficinecy in cases where we need to find elements in a sorted array.
- There are many sorting algorithms, each with its own characteristics (e.g., time complexity, space complexity, stability, etc.).
- The easiest sorting algorithm to understand and implement is selection sort (O(n^2)).
- Because sorting arrays is so common, the C++ standard library includes a function called
std::sort(in ` header), that can be invoked on array as:std::sort(std::begin(array), std::end(array));
Iterating through an array (or other structure) of data is quite common thing to do in programming. So far, we covered many different ways to do it: with loops and an index (for loops, while loops, etc.), with range-based for loops, with pointers and pointer arithmetics, etc.
#include <array>
#include <cstddef>
#include <iostream>
int main()
{
// In C++17, the type of variable arr is deduced to std::array<int, 7>
// If you get an error compiling this example, see the warning below
std::array arr{ 0, 1, 2, 3, 4, 5, 6 };
std::size_t length{ std::size(arr) };
// while-loop with explicit index
std::size_t index{ 0 };
while (index < length)
{
std::cout << arr[index] << ' ';
++index;
}
std::cout << '\n';
// for-loop with explicit index
for (index = 0; index < length; ++index)
{
std::cout << arr[index] << ' ';
}
std::cout << '\n';
// for-loop with pointer (Note: ptr can't be const, because we increment it)
for (auto ptr{ &arr[0] }; ptr != (&arr[0] + length); ++ptr)
{
std::cout << *ptr << ' ';
}
std::cout << '\n';
// range-based for loop
for (int i : arr)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}Looping using indexes is more typing than needed if we only use the index to access elements. It also only works if the container (e.g. the array) provides direct access to elements (which arrays do, but some other types of containers, such as lists, do not).
Looping with pointers and pointer arithmetic is verbose, and can be confusing to readers who don’t know the rules of pointer arithmetic. Pointer arithmetic also only works if elements are consecutive in memory (which is true for arrays, but not true for other types of containers, such as lists, trees, and maps).
Range-based for-loops are a little more interesting, as the mechanism for iterating through our container is hidden -- and yet, they still work for all kinds of different structures (arrays, lists, trees, maps, etc…). How do these work? They use iterators.
An iterator is an object designed to traverse through a container (e.g., the values in an array, the characters in a string, the elements in a list, etc.), providing access to each element along the way.
A container may provide different kind of iterators, each with its own characteristics (e.g., random access, forward only, etc.). For example, an array container could provide a forward iterator that walks though the array in forward direction, and a reverse iterator that walks though the array in reverse direction.
Once the appropriate type of iterator is created, the programmer can use the interface provided by the iterator to traverse and access elements without worring about what kind of traversal is being done or how the data is being stored in the container. Because C++ iterators typically use the same interface for traversal (e.g., operator++ to increment/move to the next element, operator* to access the current element, etc.), the programmer can use the same code to traverse different types of containers.
The simplest kind of iterator is a pointer, which (using pointer arithmetics) works for data stored sequentially in memory (e.g., arrays). Let us revisit a simple array traversal using a pointer and pointer arithmetics:
#include <array>
#include <iostream>
int main()
{
std::array arr{ 0, 1, 2, 3, 4, 5, 6 };
auto begin{ &arr[0] };
// note that this points to one spot beyond the last element
auto end{ begin + std::size(arr) };
// for-loop with pointer
for (auto ptr{ begin }; ptr != end; ++ptr) // ++ to move to next element
{
std::cout << *ptr << ' '; // Indirection to get value of current element
}
std::cout << '\n';
return 0;
}Output:
0 1 2 3 4 5 6
In this example, we defined two pointers, begin and end, that point to the first and one spot beyond the last element of the array, respectively. We then used a for-loop to traverse the array using the pointer ptr, and accessed each element using the dereference operator *. The for loop ends when ptr reaches end (that is, when ptr points to one spot beyond the last element of the array).
Since iterating is such a common operation, all standard library containers offer direct support for iteration. Instead of calculating our own begin and end pointers (by &arr[0] and &arr[0] + std::size(arr)), we simply get the begin and end points of a container using their member functions std::begin and std::end.
#include <array>
#include <iostream>
int main()
{
std::array array{ 1, 2, 3 };
// Ask our array for the begin and end points (via the begin and end member functions).
auto begin{ array.begin() };
auto end{ array.end() };
for (auto p{ begin }; p != end; ++p) // ++ to move to next element.
{
std::cout << *p << ' '; // Indirection to get value of current element.
}
std::cout << '\n';
return 0;
}This will output:
1 2 3
Additionally, the <iterator> header provides two generic std::begin and std::end functions, which can be used to get the begin and end points of a container.
Note:
std::beginandstd::endfor C-style arrays are defined in the<iterator>header, while for containers that supports iterators (e.g.,std::array,std::vector, etc.) are defined in their respective header files (e.g.,<array>,<vector>, etc.).
#include <array> // includes <iterator>, for C-style arrays
#include <iostream>
int main()
{
std::array array{ 1, 2, 3 };
// Use std::begin and std::end to get the begin and end points.
auto begin{ std::begin(array) };
auto end{ std::end(array) };
for (auto p{ begin }; p != end; ++p) // ++ to move to next element
{
std::cout << *p << ' '; // Indirection to get value of current element
}
std::cout << '\n';
return 0;
}Output:
1 2 3
Don’t worry about the types of the iterators for now, we’ll re-visit iterators in a later chapter. The important thing is that the iterator takes care of the details of iterating through the container. All we need are four things:
- A begin point
- An end point
- A way to move to the next element (
operator++) - A way to access the current element value (
operator*)
In a previous chapter, we noted that using operator< was preferred over operator!= when doing numeric comparisons in the loop condition:
for ( index = 0; index < length; ++index )For iterators instead, it is conventional to use operator!= to test whether the iterator has reached the end element of the container:
for ( auto p{ begin }; p != end; ++p )This is because some iterator types are not relationally comparable (e.g., using <, <=, >, >= is not valid for all iterator types). operator!= is always valid for all iterator types.
All type that have both begin() and end() member functions, or that can be used with std::begin() and std::end(), can be used in range-based for-loops.
#include <array>
#include <iostream>
int main()
{
std::array array{ 1, 2, 3 };
// This does exactly the same as the loop we used before.
for (int i : array)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}Under the hood, the range-based for-loop calles begin() and end() of the type to iterate over, and uses an iterator to traverse the container.
std::arrayhas bothbegin()andend()member functions, so it can be used in a range-based for-loop.- C-style arrays can be used with
std::begin()andstd::end(), so they can be used in a range-based for-loop. - Dynamic C-style arrays (or decayed C-style arrays) don't work, since there is no
std::endfunction for them (the type information does not contain the size of the array). std::vectorhas bothbegin()andend()member functions, so it can be used in a range-based for-loop.std::listhas bothbegin()andend()member functions, so it can be used in a range-based for-loop.- and others
You will learn how to add these function to your types later, so that they can be used in range-based for-loops.
Range-based for-loops are not the only thing that makes use of iterators. They are additionally used in some of the standard library algorithms, such as std::sort, std::find, std::copy, etc.
Similar to pointers and references, iterators can be left "dangling" if the elements being iterated over change address or are destroyed. When this happens, we say that the iterator is invalidated, and it will produce undefined behavior if used.
Some operations that modify the containers (e.g., adding an element to a std::vector) can have the side effect of causing the elements in the container to chnage addresses, invalidating any iterators pointing to them. Good C++ reference docuemntation should note which container operrations may or will invalidate iterators. As an example, have a look at the "Iterator invalidation" section of std::vector on cppreference.com.
Since range-based for-loops use iterators behind the scenes, we must be careful not to do anything that invalidates the iterators of the container we are actively traversing:
#include <vector>
int main()
{
std::vector v { 0, 1, 2, 3 };
for (auto num : v) // implicitly iterates over v
{
if (num % 2 == 0)
v.push_back(num + 1); // when this invalidates the iterators of v, undefined behavior will result
}
return 0;
}In this case, the push_back operation invalidates the iterators of the container we are actively traversing, leading to undefined behavior.
Another example of invalidation:
#include <iostream>
#include <vector>
int main()
{
std::vector v{ 1, 2, 3, 4, 5, 6, 7 };
auto it{ v.begin() };
++it; // move to second element
std::cout << *it << '\n'; // ok: prints 2
v.erase(it); // erase the element currently being iterated over
// erase() invalidates iterators to the erased element (and subsequent elements)
// so iterator "it" is now invalidated
++it; // undefined behavior
std::cout << *it << '\n'; // undefined behavior
return 0;
}In this case, the erase operation invalidates the iterator it, since it points to the element being erased. This will also invalidate subsequent elements, since the container's elements are stored contiguously in memory.
Invalidated iterators can be revalidated by reassigning them to a valid iterator (e.g., begin(), end(), or the return value of some other function that returns an iterator).
The erase() function returns an iterator to the elementone past the erased element (or end() if the last element was erased). Therefore, we can fix the previous code like this:
#include <iostream>
#include <vector>
int main()
{
std::vector v{ 1, 2, 3, 4, 5, 6, 7 };
auto it{ v.begin() };
++it; // move to second element
std::cout << *it << '\n';
it = v.erase(it); // erase the element currently being iterated over, set `it` to next element
std::cout << *it << '\n'; // now ok, prints 3
return 0;
}In this way, iterator it remains valid, pointing to the element after the erased element.
- Iterators is an object that is designed to traverse a container, and provide access to the elements of the container.
- The simplest type of an iterator is a pointer, which can be used to traverse an array or a string (e.g.,
auto begin{ &array[0] }; auto end{ begin + array_size };). - Standard library containers usually offer direct support for iterators, through
begin()andend()member functions, or that can be used withstd::begin()andstd::end(), so they can be used in range-based for-loops. - Prefer using
operator!=overoperator<when doing numeric comparisons in the loop condition. - Range-based for-loops use iterators behind the scenes, and can be used with any type that has both
begin()andend()member functions, or that can be used withstd::begin()andstd::end(). - Similar to references and pointers, iterators can be left "dangling" if the elements being iterated over change address or are destroyed. When this happens, we say that the iterator is invalidated, and it will produce undefined behavior if used.
- Iterator invalidation can be avoided by revalidating the iterator (e.g., reassigning it to a valid iterator, or using the return value of some function that returns an iterator).
New programmers spend a lot of time writing custom loops to perform common tasks, such as searching for an element in a container, sorting a container, counting elements, etc.
Because searching, counting, and sorting is such a common task, the C++ standard library provides a set of algorithms that can be used to perform these tasks. Additionally, these functions comes pre-tested, are efficient, work on a variaty of different container types, and many support parallelization (the ability to perform the task on multiple CPU threads).
The functionalities provided by the standard library algorithms are usually grouped in three categories:
- Inspectors: used to view (but not modify) data in a container (e.g., searching and counting elements).
- Mutators: used to modify data in a container (e.g., sorting and shuffling elements).
- Facilitators: used to generate a result based on values of the data members (e.g., objects that mutiply values, objects that determine what order pairs of elements should be sorted in, etc.).
These algorithms lives in the <algorithm> header. In this lesson, we will have a look at the most common algorithms provided by the standard library.
Note: All of these make use of iterators, so they can be used with any container that has both
begin()andend()member functions, or that can be used withstd::begin()andstd::end().
std::find searches for the first occurrence of a value in a container. It has three inputs:
- An iterator to the starting element in the sequence to search
- An iterator to the end of the sequence to search
- The value to search for
It returns an iterator to the first element that matches the value, or end() if no match is found.
For example:
#include <algorithm>
#include <array>
#include <iostream>
int main()
{
std::array arr{ 13, 90, 99, 5, 40, 80 };
std::cout << "Enter a value to search for and replace with: ";
int search{};
int replace{};
std::cin >> search >> replace;
// Input validation omitted
// std::find returns an iterator pointing to the found element (or the end of the container)
// we'll store it in a variable, using type inference to deduce the type of
// the iterator (since we don't care)
auto found{ std::find(arr.begin(), arr.end(), search) };
// Algorithms that don't find what they were looking for return the end iterator.
// We can access it by using the end() member function.
if (found == arr.end())
{
std::cout << "Could not find " << search << '\n';
}
else
{
// Override the found element.
*found = replace;
}
for (int i : arr)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}Sample run when the element is found:
Enter a value to search for and replace with: 5 234
13 90 99 234 40 80
Sample run when the element is not found:
Enter a value to search for and replace with: 0 234
Could not find 0
13 90 99 5 40 80
In this example:
std::findreturns an iterator to the first element that matches the valuesearch- If the value is not found,
std::findreturns an iterator to the end of the container (arr.end()), and we print a message to the user - If the value is found, we override the found element with the value
*found = replace
In some cases, we might want to find an element that matches some condition (e.g., string that contains a specific substring) rather than an exact value. For this, we can use std::find_if.
The std::find_if works similarly to std::find, but instead of passing a specific value to search for, we pass in a callable object (e.g., a function pointer, a lambda, etc.) that returns a boolean value. This callable object is called for each element in the container on each iteration (passing the element as an argument), and std::find_if returns an iterator to the first element for which the callable object returns true, or end() if no match is found.
In this example, we are using std::find_if to check if any elements contain the substring "nut":
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
// Our function will return true if the element matches
bool containsNut(std::string_view str)
{
// std::string_view::find returns std::string_view::npos if it doesn't find
// the substring. Otherwise it returns the index where the substring occurs
// in str.
return str.find("nut") != std::string_view::npos;
}
int main()
{
std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" };
// Scan our array to see if any elements contain the "nut" substring
auto found{ std::find_if(arr.begin(), arr.end(), containsNut) };
if (found == arr.end())
{
std::cout << "No nuts\n";
}
else
{
std::cout << "Found " << *found << '\n';
}
return 0;
}Output:
Found walnut
Here you can see that std::find_if receives a callable object (in this case, a function pointer) as the third argument, and it returns an iterator to the first element for which the callable object returns true, or end() if no match is found.
std::count and std::count_if are used to count how many occurrences of a value (or a condition) there are in a container. They receive three arguments:
- An iterator to the starting element in the sequence to search
- An iterator to the end of the sequence to search
- The value to search for (or the condition to check for)
For example, we can count how many elements contain the substring "nut" in an array of strings:
#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
bool containsNut(std::string_view str)
{
return str.find("nut") != std::string_view::npos;
}
int main()
{
std::array<std::string_view, 5> arr{ "apple", "banana", "walnut", "lemon", "peanut" };
auto nuts{ std::count_if(arr.begin(), arr.end(), containsNut) };
std::cout << "Counted " << nuts << " nut(s)\n";
return 0;
}Output:
Counted 2 nut(s)
Previously we used std::sort to sort an array in ascending order. There is a version of std::sort that takes a function as its third parameter, that allows us to sort the array in any way we want. The function takes two parameter to compare, and returns true if the first argument should come before the second argument in the sorted order.
By default, std::sort sorts the elements in ascending order (using < operator), but we can provide a custom comparison function to sort the elements in other ways.
For example, let's sort an array in reverse order using a custom comparison function named greater:
#include <algorithm>
#include <array>
#include <iostream>
bool greater(int a, int b)
{
// Order @a before @b if @a is greater than @b.
return (a > b);
}
int main()
{
std::array arr{ 13, 90, 99, 5, 40, 80 };
// Pass greater to std::sort
std::sort(arr.begin(), arr.end(), greater);
for (int i : arr)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}Output:
99 90 80 40 13 5
std::sort will sort the container by using the custom comparison function greater to compare the elements, in this way, the final sorted output will satisfy the condition greater(arr[i], arr[i + 1]) for all i in the range [0, arr.size() - 1]. To remember easily, think that the function you are passing is the condition you want to be true for all pairs of elements in the container from arr.begin() to arr.end().
Note: When we write a function name without parentheses (e.g.,
greater), it is only a function pointer, not a call to the function.std::sortwill then call the function internally for each pair of elements in the container. We will see more about this in the next chapters.
The following is for advanced readers only. Think about how a sorting function is defined (from previous selection sort):
#include <iostream>
#include <iterator>
#include <utility>
void sort(int* begin, int* end)
{
for (auto startElement{ begin }; startElement != end-1; ++startElement)
{
auto smallestElement{ startElement };
// std::next returns a pointer to the next element, just like (startElement + 1) would.
for (auto currentElement{ std::next(startElement) }; currentElement != end; ++currentElement)
{
if (*currentElement < *smallestElement)
{
smallestElement = currentElement;
}
}
std::swap(*startElement, *smallestElement);
}
}
int main()
{
int array[]{ 2, 1, 9, 4, 5 };
sort(std::begin(array), std::end(array));
for (auto i : array)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}If we would like to support a comparison function, we would need to modify the function signature to accept a comparison function as a parameter (introducing a new type: std::function<bool(int, int)>), and then pass the comparison function to the std::sort function:
void sort(int* begin, int* end, std::function<bool(int, int)> compare)In this way, sort() function can accept a third parameter, the comparison function, and use it to sort the elements in the container. But how does it work internally ?
Syntaxically, we just need to replace the comparison line (if (*currentElement < *smallestElement)) with the comparison function call (yes, we can simply use this function as a callable object, since it is a function pointer):
if (compare(*currentElement, *smallestElement))Now, the caller of sort, can decide which comparison function to use, meaning how to sort the array, since the comparison function will return true in specific cases, and false in other cases:
#include <functional> // std::function
#include <iostream>
#include <iterator>
#include <utility>
// sort accepts a comparison function
void sort(int* begin, int* end, std::function<bool(int, int)> compare)
{
for (auto startElement{ begin }; startElement != end-1; ++startElement)
{
auto smallestElement{ startElement };
for (auto currentElement{ std::next(startElement) }; currentElement != end; ++currentElement)
{
// the comparison function is used to check if the current element should be ordered
// before the currently "smallest" element.
if (compare(*currentElement, *smallestElement))
{
smallestElement = currentElement;
}
}
std::swap(*startElement, *smallestElement);
}
}
int main()
{
int array[]{ 2, 1, 9, 4, 5 };
// use std::greater to sort in descending order
// (We have to use the global namespace selector to prevent a collision
// between our sort function and std::sort.)
::sort(std::begin(array), std::end(array), std::greater{});
for (auto i : array)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}Output:
9 5 4 2 1
In this case, if std::greater returns true for a pair of elements, it means that the first element should come before the second element in the sorted order, and so it will pick the biggest element from the unsorted subarray and move it to the sorted subarray (here it is called "smallestElement" for the sake of the example, but it is the biggest element). At the end, the array will be sorted in descending order.
std::for_each takes a list as input (begin and end iterators) and a unary function (a function that takes a single argument) to apply to each element of the list. This is useful when we want to perform the same operation to every element of the list.
For example, we can double every element of an array:
#include <algorithm>
#include <array>
#include <iostream>
void doubleNumber(int& i)
{
i *= 2;
}
int main()
{
std::array arr{ 1, 2, 3, 4 };
std::for_each(arr.begin(), arr.end(), doubleNumber);
for (int i : arr)
{
std::cout << i << ' ';
}
std::cout << '\n';
return 0;
}Output:
2 4 6 8
Here you can note that the function doubleNumber has been applied to every element of the array, and receive a reference to the element as an argument, so that it can modify the element in place, without returning a new element and so avoiding copies.
To new programmers, this can seem the most unnecessary algorithm, because equivalent code with range-based for-loop is shorter and easier to understand, but it is not the case. Several benefits come with using std::for_each. Let's compare std::for_each to a range-based for-loop:
std::ranges::for_each(arr, doubleNumber); // Since C++20, we don't have to use begin() and end().
// std::for_each(arr.begin(), arr.end(), doubleNumber); // Before C++20
for (auto& i : arr)
{
doubleNumber(i);
}With std::for_each, our intentions are clear: call doubleNumber on every element of container arr. In the range-based for-loop, we need to add a new variable i, that can lead to several bugs in the code:
- There could cause an implicit conversion if we are not using
auto - We could forget the
&, and so the function would not modify the container - We could accidentally pass a variable other than
ito the function, leading to undefined behavior
All these mistakes cannot be made with std::for_each.
Additionally, std::for_each can skip elements at the beginning or end of the container, for example to skip the first element of arr, std::next can be used to advance begin to the next element:
std::for_each(std::next(arr.begin()), arr.end(), doubleNumber);
// Now arr is [1, 4, 6, 8]. The first element wasn't doubled.Additionally, like many standard library algorithms, this can be easily parallelized, achieving better performance on large containers.
Overall:
- You reduce possible errors
- You can skip elements
- You can parallelize the algorithm
Amazing! Right?
Many of the algorithms in the algorithms library make some kind of guarantee about how they will execute. Typically these are either performance guarantees, or guarantees about the order in which they will execute. For example, std::for_each guarantees that each element will only be accessed once, and in the order they appear in the container (forwards).
While most algorithms provide some kind of performance guarantee, fewer have order of execution guarantees. For such algorithms, we need to be careful not to make assumptions about the order in which elements will be accessed or processed. For example, if we were using a standard library algorithm to multiply the first value by 1, the second value by 2, the third by 3, etc, we’d want to avoid using any algorithms that didn’t guarantee a forwards sequential execution order, since it is not guaranteed that the algorithm will process the elements in the order we expect.
The following algorithms guarantee sequential execution: std::for_each, std::copy, std::copy_backward, std::move, and std::move_backward. Many other algorithms (particular those that use a forward iterator) are implicitly sequential due to the forward iterator requirement.
Best Practice: Before using a particular algorithm, make sure performance and execution order guarantees work for your particular use case.
Since having to explicitly pass arr.begin() and arr.end() can be error-prone and a bit annoying, C++20 introduced ranges, which allows us to simply pass arr to the algorithm, and it will automatically use the begin and end iterators of this container.
The algorithms library has a ton of useful functionality that can make your code simpler and more robust. We only cover a small subset in this lesson, but because most of these functions work very similarly, once you know how a few work, you can make use of most of them.
Super Best Practice: Use the algorithms library whenever possible. Rememeber: before trying to implement your own algorithm, check if the algorithms library already has it. This lesson can be used in life, not just in programming.
- Algorithms in standard library can be divided in inspectors (algorithms that only read the elements), modifiers (algorithms that modify the elements), and facilitators (algorithms that return a result based on values of the elements).
std::findis an algorithm that returns an iterator to the first element that matches the value we are looking for (orendif no such element is found). This can be called like:std::find(container.begin(), container.end(), value);std::find_ifis an algorithm that returns an iterator to the first element that matches a specific condition (orendif no such element is found). This can be called like:std::find_if(container.begin(), container.end(), condition);, by passing a function pointer (condition), a lambda.std::countandstd::count_ifare algorithms that return the number of elements that match a specific value or condition. This can be called like:std::count(container.begin(), container.end(), value);andstd::count_if(container.begin(), container.end(), condition);- You can use
std::sortto sort a container in a specific order. This can be called like:std::sort(container.begin(), container.end(), condition);, by passing a condition that compares pairs of elements (e.g.,a < b,a > b, etc.). std::for_eachis an algorithm that applies a function to every element of a container. This can be called like:std::for_each(container.begin(), container.end(), function);, by passing a function pointer (function), a lambda.- Before using algorithms, make sure performance and execution order guarantees work for your particular use case.
- In general, prefer using algorithms over range-based for-loops or raw custom loops, sicne they are well tested and provide better performance.
When you are writing code, it is important to measure the performance of your code or third party code. For example, you might want to select the best algorithm for a specific use case, or you might want to optimize your code for performance.
One eay way to measure performance is to measure the time taken by a piece of code to execute. C++11 comes with some functionality in the chrono library to measure time. However, using the chrono library is a bit arcane. The good news is that we can easily encapsulate all the timing functionalities we need into a class that we can then use in our own programs.
Here is the class:
#include <chrono> // for std::chrono functions
class Timer
{
private:
// Type aliases to make accessing nested type easier
using Clock = std::chrono::steady_clock;
using Second = std::chrono::duration<double, std::ratio<1> >;
std::chrono::time_point<Clock> m_beg { Clock::now() };
public:
void reset()
{
m_beg = Clock::now();
}
double elapsed() const
{
return std::chrono::duration_cast<Second>(Clock::now() - m_beg).count();
}
};This class Timer has a member variable m_beg of type std::chrono::time_point<Clock> that stores the time when the timer was started. It has two member functions: reset and elapsed. With reset we reset the start time of the timer to the current time, and with elapsed we return the time elapsed since the timer was started.
In order to use it, we instantiate a Timer object at the top of our main function (or whenever we want to start timing), and then we can use the elapsed() member function to get the time elapsed since the timer was started:
#include <iostream>
int main()
{
Timer t;
// Code to time goes here
std::cout << "Time elapsed: " << t.elapsed() << " seconds\n";
return 0;
}Now, let's use this in an actual example where we sort an array of 10000 elements, using our previously coded sort algorithm:
#include <array>
#include <chrono> // for std::chrono functions
#include <cstddef> // for std::size_t
#include <iostream>
#include <numeric> // for std::iota
const int g_arrayElements { 10000 };
class Timer
{
private:
// Type aliases to make accessing nested type easier
using Clock = std::chrono::steady_clock;
using Second = std::chrono::duration<double, std::ratio<1> >;
std::chrono::time_point<Clock> m_beg{ Clock::now() };
public:
void reset()
{
m_beg = Clock::now();
}
double elapsed() const
{
return std::chrono::duration_cast<Second>(Clock::now() - m_beg).count();
}
};
void sortArray(std::array<int, g_arrayElements>& array)
{
// Step through each element of the array
// (except the last one, which will already be sorted by the time we get there)
for (std::size_t startIndex{ 0 }; startIndex < (g_arrayElements - 1); ++startIndex)
{
// smallestIndex is the index of the smallest element we’ve encountered this iteration
// Start by assuming the smallest element is the first element of this iteration
std::size_t smallestIndex{ startIndex };
// Then look for a smaller element in the rest of the array
for (std::size_t currentIndex{ startIndex + 1 }; currentIndex < g_arrayElements; ++currentIndex)
{
// If we've found an element that is smaller than our previously found smallest
if (array[currentIndex] < array[smallestIndex])
{
// then keep track of it
smallestIndex = currentIndex;
}
}
// smallestIndex is now the smallest element in the remaining array
// swap our start element with our smallest element (this sorts it into the correct place)
std::swap(array[startIndex], array[smallestIndex]);
}
}
int main()
{
std::array<int, g_arrayElements> array;
std::iota(array.rbegin(), array.rend(), 1); // fill the array with values 10000 to 1
Timer t;
sortArray(array);
std::cout << "Time taken: " << t.elapsed() << " seconds\n";
return 0;
}On the author’s machine, three runs produced timings of 0.0507, 0.0506, and 0.0498. So we can say around 0.05 seconds.
Now, let's use std::sort to sort the array:
#include <algorithm> // for std::sort
#include <array>
#include <chrono> // for std::chrono functions
#include <cstddef> // for std::size_t
#include <iostream>
#include <numeric> // for std::iota
const int g_arrayElements { 10000 };
class Timer
{
private:
// Type aliases to make accessing nested type easier
using Clock = std::chrono::steady_clock;
using Second = std::chrono::duration<double, std::ratio<1> >;
std::chrono::time_point<Clock> m_beg{ Clock::now() };
public:
void reset()
{
m_beg = Clock::now();
}
double elapsed() const
{
return std::chrono::duration_cast<Second>(Clock::now() - m_beg).count();
}
};
int main()
{
std::array<int, g_arrayElements> array;
std::iota(array.rbegin(), array.rend(), 1); // fill the array with values 10000 to 1
Timer t;
std::ranges::sort(array); // Since C++20
// If your compiler isn't C++20-capable
// std::sort(array.begin(), array.end());
std::cout << "Time taken: " << t.elapsed() << " seconds\n";
return 0;
}On the author’s machine, this produced results of: 0.000693, 0.000692, and 0.000699. So basically right around 0.0007.
Then, tsd::sort is bout 100 times faster than our own implementation. Crazy right?
Timing a run of your program is fairly straightforward, but your results can be significantly impacted by a number of things, and it’s important to be aware of how to properly measure and what things can impact timing.
Here are some things that can impact the performance of your program:
- Make sure you are using a release build target, not a debug build target. Debug build targets typically turn optimization off, and those can have a significant impact on performance. For example, with a debug build target, the author’s machine took
0.0235seconds, 33 times slower than the release build target. - Your timing results may be influenced by other things your system may be doing in the background. Make sure your maching is not doing anything CPU, memory, or hard drive intensive (e.g., playing a game, searching for a file, etc.). The more app you can shut down before running the measurement, the better.
- If your program uses a random number generator, the particular sequence of random numbers generated can have a significant impact on performance. For example, if you’re sorting an array filled with random numbers, the results will likely vary from run to run because the number of swaps required to sort the array will vary from run to run. To avoid this, you can use a random number generator that uses a fixed seed (e.g.,
std::mt19937with a fixed seed). - Make sure you are you are not timing waiting for user input, as how long the user takes to input is something you cannot control. If user input is required, consider adding some way to provide that input that does not wait on the user (e.g. command line, from a file, having a code path that routes around the input).
When measuring the performance of your program, gather at least 3 results. If the results are all similar, these likely represent the actual performance of your program on that machine. Otherwise, continue to take measurements until you have a cluster of similar results (and understand which other results are outliers). It’s not uncommon to have one or more outliers due to your system doing something in the background during some of those runs.
If your results have a lot of variance, your program is likely either being significantly affected by other things happening on the system, or by the effect of randomization within your program (e.g., sorting an array of random numbers).
Because performance measurements are impacted by so many things (particularly hardware speed, but also OS, apps running, etc…), absolute performance measurements (e.g. “the program runs in 10 seconds”) are generally not that useful outside of understanding how well the program runs on one particular machine you care about. On a different machine, that same program may run in 1 second, 10 seconds, or 1 minute. It’s hard to know without actually measuring across a spectrum of different hardware.
However, on a single machine, relative performance measurements can be useful. We can gather performance results from several different variants of a program to determine which variant is the most performant. For example, if variant 1 runs in 10 seconds and variant 2 runs in 8 seconds, variant 2 is probably going to be faster on all similar machines regardless of the absolute speed of that machine.
After measuring the second variant, a good sanity check is to measure the first variant again. If the results of the first variant are consistent with your initial measurements for that variant, then the result of both variants should be reasonably comparable. For example, if variant 1 runs in 10 seconds, and variant 2 runs in 8 seconds, and then we measure variant 1 again and get 10 seconds, then we can reasonably conclude that the measurements for both variants were fairly measured, and that variant 2 is faster. However, if the results of the first variant are no longer consistent with your initial measurements for that variant, then something has happened on the machine that is now affecting performance, and it will be hard to tell whether differences in measurement are due to the variant or due to the machine itself. In this case, it’s best to discard the existing results and re-measure.
- Measuting the performance of a program is a complex topic, and there are many things that can impact the results.
- You can use the
std::chronolibrary to measure the performance of a program (using a timer class, or thestd::chrono::high_resolution_clockclass). - Use release build target (not debug build target) to measure the performance of a program.
- Background processes can impact the performance of your program, so try to measure the performance of your program in a clean environment.
- Randomization can impact the performance of your program, so try to measure the performance of your program in a clean environment.
- Avoid measuring the performance of your program when the user is interacting with it.
- Get at least 3 results when measuring the performance of a program.
- If the results are consistent, then the results are likely representative of the actual performance of the program.
- If the results have a lot of variance, then the results could be impacted by other things happening on the machine, or by the effect of randomization within your program.
As always, have a look at the original summary, and good luck for the quizzes!
PS: solutions are inside exercises/sx-questions folder. Enjoy :)