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 

  1. Non-Modifying sequence operation

  2. Sorting And related operations

  3. Mutating sequence operations

  4. General numeric operations

  5. 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

  1. Complexity
  2. Degenerative cases i.e. empty cases 
  3. Think about iterator category

 

 

Feedback

T222 of COMP6771 22T2 - 2.3 - STL Algorithms

By imranrazzak

T222 of COMP6771 22T2 - 2.3 - STL Algorithms

  • 62