COMP6771
Advanced C++ Programming
Week 2.3
STL Algorithms
Ocean of Algorithms
- STL Algorithms are functions that execute an algorithm on an abstract notion of an iterator.
- In this way, they can work on a number of containers as long as those containers can be represented via a relevant iterator.
- Come with C++ Compiler
- template function not class
- useful but generic
- useful name
- other libraries than STL such as ASL, Boost algorithm library, and several others
STL: Algorithms
-
STL Algorithms makes code expressive
- raising abstraction level
- Spectacular
-
avoid common mistakes
- empty loop
- off by one
- complexity ?
- Declarative syntax: avoid loop
- Iterate over small sequence
- Common use (standard, basic), can build on top of it
- Whatever compiler?
- work on a number of containers as long as those containers can be represented via a relevant iterator.
- Designed by Experts
Why STL: Algorithms?
Why Algorithms?
-
Often more efficient than handwritten loops
-
tested and debugged
-
Cleaner and more clearly abstracted than raw loop
- min_element(vec.begin(), vec.end());
-
Contains side effect inside a clear interface
-
Prevents accidental leakage
-
Eases reasoning about functionality and reasoning about post condition
-
Ease reasoning about surrounding code
-
Less likely to fail
-
Easier
- easier to write code,
- easier to debug code
What's the best way to sum a vector of numbers?
C-style?
Many lines of code?
May have side effects ?
#include <iostream>
#include <vector>
int main() {
std::vector<int> nums{1,2,3,4,5};
int sum = 0;
for (int i = 0; i <= nums.size(); ++i) {
sum += i;
}
std::cout << sum << "\n";
};
Simple Example
#include <iostream>
#include <vector>
int main() {
std::vector<int> nums{1,2,3,4,5};
auto sum = 0;
for (auto it = nums.begin(); it != nums.end(); ++it) {
sum += *it;
}
std::cout << sum << "\n";
}
What's the best way to sum a vector of numbers?
Via an iterator? Or for-range?
Simple Example
#include <iostream>
#include <vector>
int main() {
std::vector<int> nums{1,2,3,4,5};
int sum = 0;
// Internally, this uses begin and end,
// but it abstracts it away.
for (const auto& i : nums) {
sum += i;
}
std::cout << sum << "\n";
}
demo207-simple-sum.cpp
demo208-simple-sum.cpp
(This is the underlying mechanics)
#include <iostream>
#include <numeric>
#include <vector>
int main() {
std::vector<int> nums{1,2,3,4,5};
int sum = std::accumulate(nums.begin(), nums.end(), 0);
std::cout << sum << "\n";
}
What's the best way to sum a vector of numbers?
Via use of an STL Algorithm
Simple Example
// What type of iterator is required here?
template <typename T, typename Container>
T sum(iterator_t<Container> first, iterator_t<Container> last) {
T total;
for (; first != last; ++first) {
total += *first;
}
return total
}
demo209-accum.cpp
#include <iostream>
#include <numeric>
#include <vector>
int main() {
std::vector<int> v{1,2,3,4,5};
int sum = std::accumulate(v.begin(), v.end(), 0);
// What is the type of std::multiplies<int>()
int product = std::accumulate(v.begin(), v.end(), 1, std::multiplies<int>());
auto midpoint = v.begin() + (v.size() / 2);
// This looks a lot harder to read. Why might it be better?
auto midpoint11 = std::next(v.begin(), std::distance(v.begin(), v.end()) / 2);
int sum2 = std::accumulate(v.begin(), midpoint, 0);
std::cout << sum << "\n";
}
We can also use algorithms to:
- Find the product instead of the sum
- Sum only the first half of elements
More examples
demo211-algos.cpp
#include <iostream>
#include <vector>
int main() {
std::vector<int> nums{1,2,3,4,5};
auto it = std::find(nums.begin(), nums.end(), 4);
if (it != nums.end()) {
std::cout << "Found it!" << "\n";
}
}
We can also use algorithms to:
- Check if an element exists
More examples
demo212-find.cpp
for each
- Consider:
- Number of comparisons for binary search on a vector is O(log N)
- Number of comparisons for binary search on a linked list is O(N log N)
- The two implementations are completely different
- We can call the same function on both of them
- It will end up calling a function have two different overloads, one for a forward iterator, and one for a random access iterator
- Trivial to read
- Trivial to change the type of a container
Performance & Portability
#include <algorithm>
#include <iostream>
#include <list>
#include <vector>
int main() {
// Lower bound does a binary search, and returns the first value >= the argument.
std::vector<int> sortedVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::lower_bound(sortedVec.begin(), sortedVec.end(), 5);
std::list<int> sortedLinkedList{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::lower_bound(sortedLinkedList.begin(), sortedLinkedList.end(), 5);
}
demo213-bound.cpp
Algorithms with output sequences
#include <iostream>
#include <vector>
char to_upper(unsigned char value) {
return static_cast<char>(std::toupper(static_cast<unsigned char>(value)));
}
int main() {
std::string s = "hello world";
// Algorithms like transform, which have output iterators,
// use the other iterator as an output.
auto upper = std::string(s.size(), '\0');
std::transform(s.begin(), s.end(), upper.begin(), to_upper);
}
demo214-transform.cpp
Gives you an output iterator for a container that adds to the end of it
Back Inserter
#include <iostream>
#include <vector>
char to_upper(char value) {
return static_cast<char>(std::toupper(static_cast<unsigned char>(value)));
}
int main() {
std::string s = "hello world";
// std::for_each modifies each element
std::for_each(s.begin(), s.end(), toupper);
std::string upper;
// std::transform adds to third iterator.
std::transform(s.begin(), s.end(), std::back_inserter(upper), to_upper);
}
demo215-inserter.cpp
Classes of STL Algorithms
-
Non-Modifying sequence operation
-
Sorting And related operations
-
Mutating sequence operations
-
General numeric operations
-
General C Algorithms
Non-Modifying Sequence STL
-
Dont modify the input sequence
-
Dont emit a result sequence
-
no impact on input sequence
-
Function object, if present may impact through modification of itself,
-
e.g. for_each, all_of, any_of find, find_end, find_first_of, search, equal, count
-
Mutating Sequence Operation
-
Dont modify input sequence except where the output overlap input resulting in modification
-
Emits an output sequence of results
-
Output sequence may overlap with input for certain algorithms (transform)
-
Algorithms will explicitly cause side effect in output sequence
-
function object, if present may impact by modifying itself or its environment. it should not modify the input or output
- Copy (copy_n, copy_if, copy_backward), move, swap_range, transform, fill, rotate, unique, remove, reserve, partition, generate
Sorting and Related Operations
-
Mix of non-modifying and mutating
-
mutating operation modify sequence in place ( sort) or emits output to output sequence (merge)
-
Default compare function is operator <
-
Explicit compare function objects, if supplied must not modify
-
e.g. Sorting (sort, stable_sort, partial sort, lower_bound,
-
heap operations (push heap, pop heap etc)
-
minimum and max
-
merge
-
operation of sorted containers
-
General Numeric
-
Library of algorithms for number operations
-
consist of components for complex number type, random number generation
-
e.g. accumlate, inner_product, partial_sum, iota, adjacent_difference
-
C Library Algorithms
All discussed earlier can what thiese can do :)
bsearch, qsort
for_each and transform
generic algorithms
apply operation to each element in order
very similar complexity?
for_each
-
apply operation on each element in sequence
-
non-modifying sequence operation: no output: relies on function for mutation
-
no-side effect by for each, however, function may
-
returns a moved copy of function object
#include <iostream>
#include <numeric>
#include <vector>
int main() {
std::vector<int> nums{1,2,3,4,5};
int sum = std::accumulate(nums.begin(), nums.end(), 0);
std::cout << sum << "\n";
}
transform
-
apply operation on each element in sequence
-
non-modifying sequence operation: no output: relies on function for mutation
-
if the input range(s) and result range are the same, or overlap mutates object in-place
-
algorithms side effect, however, function may not
-
explicity generate output range, hence
-
returns iterator pointing one past last element in result range
- A function that can be defined inside other functions
-
Can be used with std::function<ReturnType(Arg1, Arg2)> (or auto)
- It can be used as a parameter or variable
- No need to use function pointers anymore
Lambda Functions
#include <iostream>
#include <vector>
int main() {
std::string s = "hello world";
// std::for_each modifies each element
std::for_each(s.begin(), s.end(), [] (char& value) { value = std::toupper(value); });
}
demo216-lambda1.cpp
- Anatomy of a lambda function
- Lambdas can be defined anonymously, or they can be stored in a variable
Lambda Functions
[](card const c) -> bool {
return c.colour == 4;
}
[capture] (parameters) -> return {
body
}
- This doesn't compile
- The lambda function can get access to the scope, but does not by default
- The scope is accessed via the capture []
Lambda Captures
#include <iostream>
#include <vector>
void add_n(std::vector<int>& v, int n) {
std::for_each(v.begin(), v.end(), [n] (int& val) { val = val + n; });
}
int main() {
std::vector<int> v{1,2,3};
add_n(v, 3);
}
demo217-lambda2.cpp
Operation | Output | Input | Forward | Bidirectional | Random Access |
---|---|---|---|---|---|
Read | =*p | =*p | =*p | =*p | |
Access | -> | -> | -> | -> [] | |
Write | *p= | *p= | *p= | *p= | |
Iteration | ++ | ++ | ++ | ++ -- | ++ -- + - += -= |
Compare | == != | == != | == != | == != < > <= >= |
More powerful
Forward
Bidir.
Random
"->" no longer specified as of C++20
Iterator Categories
Input
Output
An algorithm requires certain kinds of iterators for their operations
- input: find(), equal()
- output: copy()
- forward: replace(), binary_search()
- bi-directional: reverse()
- random: sort()
A container's iterator falls into a certain category
- forward: forward_list
- bi-directional: map, list
- random: vector, deque
stack, queue are container adapters, and do not have iterators
Iterator Categories
Writing your Own Algo.
Writing your own
Writing your own
-
Write what you need
-
More general
-
stepwise refinement, testing, debugging
Tips
- Complexity
- Degenerative cases i.e. empty cases
- Think about iterator category
Feedback
T222 of COMP6771 22T2 - 2.3 - STL Algorithms
By imranrazzak
T222 of COMP6771 22T2 - 2.3 - STL Algorithms
- 62