License : Creative Commons Attribution 4.0 International (CC BY-NC-SA 4.0)
Copyright : Frédéric Pennerath, CentraleSupelec
Last modified : April 19, 2024 10:22
Link to the source : algorithms.md

Table of contents

Design principles

Design principles

Iterators bridges algorithms and containers

Iterators

The different concepts of iterators

The standard library has identified different concepts of iterators according to their capacities


Concepts of iterators

Iterators of containers

Iterators of containers populate these concepts.


Iiterators of containers

Algorithms

Every algorithm takes as inputs iterators with various requirements


Algorithm requirements

Remarks

Some examples of implementation

Example of std::find working on InputIterator

template<typename InputIt, typename T> 
InputIt find(InputIt it, InputIt end, const T& elt) {
  for(; it != end; ++it) if(*it == elt) break;
  return it;
}

std::list<int> L = { 5, 3, 9, 1 };
auto it = std::find(L.begin(), L.end(), 9);
if(it != L.end()) L.erase(it); // L now contains { 5, 3, 1 }

Example of std::copy working on OutputIterator (and InputIterator)

template<typename InputIt, typename OutputIt>
OutputIt copy(InputIt it, InputIt end, OutputIt outIt) {
  for (; it != end; ++it) *out++ = *it;
  return outIt;
}

std::list<int> L = { 5, 3, 9, 1 };
std::vector<int> V{ L.size() }; // std::copy does not create new cells. 
                                // Ensure there is enough room before calling it.                               
std::copy(L.begin(), L.end(), V.begin()); // L now contains { 5, 3, 9, 1 }

Example of std::unique working on ForwardIterator

template<typename FwdIt> 
FwdIt unique(FwdIt it, FwdIt end) {
  FwdIt lastWrite = it;
  for(; it != end; ++it)
    if(*lastWrite != *it) { 
      ++lastWrite; 
      *lastWrite = std::move(*it);
    }
  return ++lastWrite;
}

std::vector<int> V = { 5, 3, 3, 9, 1, 1, 1 };
auto new_end = std::unique(V.begin(), V.end()); 
V.resize(new_end - V.begin()); // V now contains { 5, 3, 9, 1 }

Example of std::reverse working on BidirectionalIterator

template<typename BidIt> 
void reverse(BidIt begin, BidIt end) {
  for(; begin != end; ++begin) {
    --end;
    if(begin == end)
      break;
    else
      std::swap(*begin, *end);
  }
}

std::list<int> L = { 5, 3, 9, 1 };
std::reverse(L.begin(), L.end()); // L now contains { 1, 9, 3, 5 }

Example of std::shuffle working on RandomIterator

template<typename RandIt, typename RandomGen>
void shuffle(RandIt it, RandIt end, RandomGen gen) {
  std::uniform_real_distribution<double> dist(0.0, 1.0);

  int index = 0;
  for(; it != end; ++it) {
    int i = static_cast<int>(dist(gen) * ++index); // Integer between 0 and index
    std::swap(*it, *(begin + i));
  }
}

#include <random>
std::mt19937 gen(std::random_device{}());
 
std::vector<int> V = { 5, 3, 9, 1 };
std::shuffle(v.begin(), v.end(), gen); // V is shuffled, e.g. { 3, 1, 9, 5 }

The iterator library

The iterator library defined in header <iterator> provides different tools related to iterators.

Iterator traits and concepts

The iterator library defines iterator traits that provide types and category of an iterator

#include <iterator>

std::iterator_traits<int *>::value_type value;      // value is of type int
std::iterator_traits<int *>::reference ref = value; // value is of type int&

std::iterator_traits<int *>::iterator_category cat1; // cat1 is of type std::contiguous_iterator_tag
std::iterator_traits<std::list<int>>::iterator_category cat2; // cat2 is of type std::bidirectional_iterator_tag

Iterator categories are useful to optimize algorithms: example of std::advance

template<typename Iter>
void advance(Iter& it, ptrdiff_t dist) {
  using category = typename std::iterator_traits<Iter>::iterator_category;
 
  if constexpr (std::is_base_of_v<std::random_access_iterator_tag, category>)
    it += dist;
  else
    if(dist >= 0)
      for(; dist > 0; --dist) ++it;
    else
      if constexpr (std::is_base_of_v<std::bidirectional_iterator_tag, category>)
        for(; dist < 0; ++dist) --it;
      else
        throw std::domain_error("negative count not supported");
}

Iterator adaptors

The iterator library provides various iterator adaptors. Here are some examples.

Input stream iterator adaptor

An input iterator is an example of input iterator that is not a forward iterator:

#include <iostream>
#include <sstream>
#include <iterator>

void grab_doubles(std::istream_iterator<double>& it) {
  std::istream_iterator<double> end;
  for(; it != end; ++it) std::cout << ' ' << *it;
  std::cout << std::endl;
}

std::istringstream is("1 2 3.14 4 five 6");
std::istream_iterator<double> it1(is);
std::istream_iterator<double> it2(is);

std::string myString("1 2 3.14 4 five 6 ");
std::istringstream is(myString);
std::istream_iterator<double> it1(is);
std::istream_iterator<double> it2(it1);
  std::cout << "it1:"; grab_doubles(it1);
  std::cout << "it2:"; grab_doubles(it2);
  return 0;
}

The console output illustrates a case where it1 == it2 does not imply *(++it1) == *(++it2)!

it1: 1 3.14 4
it2: 1

Output stream iterator adaptor

The output stream iterator is the dual of input stream iterators.

std::istringstream is("1 2 3.14 4");
std::ostringstream os;

std::istream_iterator<double> in(is);
std::ostream_iterator<double> out(os, " "); // Second argument is the separator in the output stream

std::istream_iterator<double> end;
for(; in != end; ++in) *out++ = 2 * *in;

std::cout << " input: " << is.str() << std::endl;
std::cout << "output: " << os.str() << std::endl;
 input: 1 2 3.14 4
output: 2 4 6.28 8 

(Back/Front) insert iterator adaptor

std::vector<double> read_coefs(std::istream& in) {
  std::vector<double> result;
  std::istream_iterator<double> it(in), end;
  
  std::back_insert_iterator insert(result);
  std::copy(it, end, insert); // Here push_back() is called
  
  return result;
}

std::istringstream is("1 2 3.14 4");
std::vector<double> V = read_coefs(is); // V contain the coefficients of the string

Many other iterator adaptors exist like:

Ranges and views (C++20)


The goal of the range library is to enable loops similar to modern functional languages (Python, Scala, etc).

Ranges


A Range R of type T is an abstract collection of elements of type T that can be enumerated
using iterators returned by function templates std::begin(R) and std::end(R).

Remarks

#include <algorithm>

std::array<int> numbers {4, 5, 8, 3, 4, 1}; 

std::ranges::sort(numbers);
// Instead of
std::sort(numbers.begin(), numbers.end());

Views



A View V is a Range that can be copied/moved in constant time.

Remarks

#include <ranges>

double temperatures[] { 5, -3, 2, -4, -5, -3, 4, 6 };

for(double temp : temperatures                                      // Static C arrays are ranges
    | std::views::filter([](auto& t) { return t < 0; })             // Only keep negative temperatures
    | std::views::transform([](auto& t) { return t + 273.15; }))    // Convert from Celsius to Kelvin
  std::cout << ' ' << temp << 'K';

Global view as a summary


Sequence

Frédéric Pennerath,