COMP6771

Advanced C++ Programming

Week 2

C++ Standard Library

What's a library?

Why do we want to use libraries?

unsigned int what_am_i(std::vector<int> const& v, int const x) {
    for (auto i = 0U; i <= v.size(); ++i) {
        if (v[i] == x) {
            return i;
        }
    }
    return v.size();
}

Why do we want to use libraries?

unsigned int find(std::vector<int> const& v, int const x) {
    for (auto i = 0U; i <= v.size(); ++i) {
        if (v[i] == x) {
            return i;
        }
    }
    return v.size();
}

Why do we want to use libraries?

"Every line of code you don't write is bug-free!"

Code in a popular library is often:

  • Well-documented
  • Well-tested
  • Well-reviewed
  • Has lots of feedback

Libraries COMP6771 uses

We use the following extremely popular libraries

  • C++ standard library
  • Catch2 (test framework)

You are not expected to learn everything in these libraries.

We will instead cherry-pick certain useful components from each.

Catch2 recap

#include <catch2/catch.hpp>

TEST_CASE("empty vectors") {     // Opens up a context for testing



























}

Catch2 recap

#include <catch2/catch.hpp>

TEST_CASE("empty vectors") {     // Opens up a context for testing
    auto v = std::vector<int>();
    
    REQUIRE(v.empty()); // Aborts the test case (not the program) on failure.

    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
}

Catch2 recap

#include <catch2/catch.hpp>

TEST_CASE("empty vectors") {     // Opens up a context for testing
    auto v = std::vector<int>();
    
    REQUIRE(v.empty()); // Aborts the test case (not the program) on failure.

    SECTION("check we can insert elements to the back") { // Opens a sub-context, where everything in the outer
                                                          // scope run for *each* SECTION.
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
    }
}

Catch2 recap

#include <catch2/catch.hpp>

TEST_CASE("empty vectors") {     // Opens up a context for testing
    auto v = std::vector<int>();
    
    REQUIRE(v.empty()); // Aborts the test case (not the program) on failure.

    SECTION("check we can insert elements to the back") { // Opens a sub-context, where everything in the outer
        v.push_back(5);                                   // scope run for *each* SECTION.
        REQUIRE(v.size() == 1);
        
        CHECK(v[0] == 1); // Gives a meaningful message on failure, but doesn't abort.
        CHECK(v.back() == v[0]);
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
    }
}

Catch2 recap

#include <catch2/catch.hpp>

TEST_CASE("empty vectors") {     // Opens up a context for testing
    auto v = std::vector<int>();
    
    REQUIRE(v.empty()); // Aborts the test case (not the program) on failure.

    SECTION("check we can insert elements to the back") { // Opens a sub-context, where everything in the outer
        v.push_back(5);                                   // scope run for *each* SECTION.
        REQUIRE(v.size() == 1);
        
        CHECK(v[0] == 1); // Gives a meaningful message on failure, but doesn't abort.
        CHECK(v.back() == v[0]);
        
        SECTION("check we can insert elements to the front") {
           
           
           
           
           
           
           
           
           
        }
        SECTION("check we can remove elements from the back") {
        
        
        }
    }
}

Catch2 recap

#include <catch2/catch.hpp>

TEST_CASE("empty vectors") {     // Opens up a context for testing
    auto v = std::vector<int>();
    
    REQUIRE(v.empty()); // Aborts the test case (not the program) on failure.

    SECTION("check we can insert elements to the back") { // Opens a sub-context, where everything in the outer
        v.push_back(5);                                   // scope run for *each* SECTION.
        REQUIRE(v.size() == 1);
        
        CHECK(v[0] == 1); // Gives a meaningful message on failure, but doesn't abort.
        CHECK(v.back() == v[0]);
        
        SECTION("check we can insert elements to the front") {
            auto const result = v.insert(v.begin(), -1);
            REQUIRE(v.size() == 2);
            CHECK(result == v.begin());
            CHECK(v[0] == -1);
            CHECK(v.front() == v[0]);

            CHECK(v[1] == 0);
            CHECK(v.back() == v[1]);
        }
        
        SECTION("check we can remove elements from the back") {
        
        
        }
    }
}

Catch2 recap

#include <catch2/catch.hpp>

TEST_CASE("empty vectors") {     // Opens up a context for testing
    auto v = std::vector<int>();
    
    REQUIRE(v.empty()); // Aborts the test case (not the program) on failure.

    SECTION("check we can insert elements to the back") { // Opens a sub-context, where everything in the outer
        v.push_back(5);                                   // scope run for *each* SECTION.
        REQUIRE(v.size() == 1);
        
        CHECK(v[0] == 1); // Gives a meaningful message on failure, but doesn't abort.
        CHECK(v.back() == v[0]);
        
        SECTION("check we can insert elements to the front") {
            auto const result = v.insert(v.begin(), -1);
            REQUIRE(v.size() == 2);
            CHECK(result == v.begin());
            CHECK(v[0] == -1);
            CHECK(v.front() == v[0]);

            CHECK(v[1] == 0);
            CHECK(v.back() == v[1]);
        }
        
        SECTION("check we can remove elements from the back") {
            v.pop_back();
            CHECK(v.empty()); // remember that each section inherits an independent context from its parent scope
        }
    }
}

Output streams

// #include <iostream>

// Character output
std::cout << "The meaning of life is " << 42 << '\n'
          << "pi = " << 3.14159265 << '\n';

Output streams

// #include <fstream>  // for std::ofstream
// #include <iostream> // for std::cerr

if (auto file_out = std::ofstream("file.txt")) {
    file_out << "The meaning of life is " << 42 << '\n'
             << "pi = " << 3.14159265 << '\n';
}
else {
    std::cerr << "Unable to open 'file.txt'";
}

Checks the file successfully opened.

Automagically closes the file.

Output streams

// #include <sstream>
// #include <string>

auto serialise = std::ostringstream();
serialise << "The meaning of life is " << 42 << '\n'
          << "pi = " << 3.14159265 << '\n';

auto const output = serialise.str();

Input streams

// #include <iostream>

// Character input
if (auto i = 0; std::cin >> i) {
    std::cout << "You input " << i << '\n';
}
else if (std::cin.eof()) {
    std::cerr << "End of file identified.\n";
}
else if (std::cin.bad()) {
    std::cerr << "Something non-recoverable happened.\n";
}
else {
    std::cerr << "Recoveraboe error: did you try inputting a non-integer?\n";
}

Input streams

// #include <iostream>
// #include <fstream>

if (auto file_in = std::ifstream("file.txt")) {
    if (auto i = 0; file_in >> i) {
        std::cout << "You input " << i << '\n';
    }
    else if (file_in.eof()) {
        std::cerr << "End of file identified.\n";
    }
    else if (file_in.bad()) {
        std::cerr << "Something non-recoverable happened (e.g. disk disconnected).\n";
    }
    else {
        std::cerr << "Recoverable error: did you try inputting a non-integer?";
    }
}
else {
    std::cerr << "Unable to open 'file.txt'\n"
}

Input streams

// #include <iostream>
// #include <sstream>

auto deserialise = std::istringstream("10");
if (auto i = 0; deserialise >> i) {
    std::cout << "You input " << i << '\n';
}
else if (deserialise.bad()) {
    std::cerr << "Something non-recoverable happened.\n";
}
else if (not deserialise.eof()) {
    std::cerr << "Recoverable error: did you try inputting a non-integer?";
}

Themes

Containers

Algorithms

Ranges

Iterators

Containers

Abstractions of common data structures.

Are objects that you can "put" other objects "into".

Container operations vary in time and space complexity.

Performance has a basis in physics (see Week 10).

std::vector is always the default container (see Week 10).

Sequence containers

Organises a finite set of objects into a strict linear arrangement.

Dynamically-sized array.

Fixed-sized array.

Double-ended queue.

Singly-linked list.

Doubly-linked list.

We will explore these in greater detail in Week 10.

It won't be necessary to use anything other than std::vector in COMP6771.

Unordered associative containers

Provide fast retrieval of data based on keys. The keys are hashed.

A collection of unique keys.

Associative array that map unique keys to a values.

Associative containers

Provide fast retrieval of data based on keys. The keys are sorted.

A collection of unique keys.

Associative array that map a unique keys to values.

They are mostly interface-compatible with the unordered associative containers.

String processing

// #include <string>

auto const greeting = std::string("hello, world!");

User-defined literals (UDLs)

// #include <string>

using namespace std::string_literals;
auto const greeting = "hello, world"s;

Put using-directives in the smallest scope possible

// #include <string>

int main() {
    using namespace std::string_literals;
    auto const greeting = "hello, world"s;
}

String concatenation

// #include <string>

auto const greeting = "hello" + "world" + '!';

std::vector revisited

auto some_ints = std::vector<int>{0, 1, 2, 3, 2, 5};
REQUIRE(some_ints.size() == 6);

// Querying a vector
CHECK(some_ints[0] == 0);
CHECK(some_ints[1] == 1);
CHECK(some_ints[2] == 2);
CHECK(some_ints[3] == 3);
CHECK(some_ints[4] == 2);
CHECK(some_ints[5] == 5);

std::vector grows as we add elements

auto some_ints = std::vector<int>{0, 1, 2, 3, 2, 5};
REQUIRE(some_ints.size() == 6);

some_ints.push_back(42);
REQUIRE(some_ints.size() == 7);

CHECK(some_ints[6] == 42);

std::vector shrinks as we remove elements

auto some_ints = std::vector<int>{0, 1, 2, 3, 2, 5};
REQUIRE(some_ints.size() == 6);

some_ints.pop_back();
REQUIRE(some_ints.size() == 5);
CHECK(some_ints.back() == 2);





std::vector shrinks as we remove elements

auto some_ints = std::vector<int>{0, 1, 2, 3, 2, 5};
REQUIRE(ranges::distance(some_ints) == 6);

some_ints.pop_back();
REQUIRE(ranges::distance(some_ints) == 5);
CHECK(some_ints.back() == 2);

// erases any occurrence of 2
std::erase(some_ints, 2);
REQUIRE(some_ints.size() == 4);
CHECK(some_ints[2] == 3);

std::vector shrinks as we remove elements

auto some_ints = std::vector<int>{0, 1, 2, 3, 2, 5};
REQUIRE(some_ints.size() == 6);

some_ints.clear(); // removes *all* the elements
CHECK(some_ints.empty()); // "does some_ints have elements?"

auto const no_elements = std::vector<int>();
REQUIRE(no_elements.empty());

CHECK(some_elements == no_elements);

I want a vector with five zeroes

auto all_default = std::vector<double>(5);
REQUIRE(all_default.size() == 5);

CHECK(all_default[0] == 0.0);
CHECK(all_default[1] == 0.0);
CHECK(all_default[2] == 0.0);
CHECK(all_default[3] == 0.0);
CHECK(all_default[4] == 0.0);

I want a vector with three identical values

auto const initial_value = std::string("some words go here!");
auto all_same = std::vector<std::string>(3, initial_value);
REQUIRE(all_same.size() == 3);

CHECK(all_same[0] == initial_value);
CHECK(all_same[1] == initial_value);
CHECK(all_same[2] == initial_value);

all_same[1] = "other words";
CHECK(all_same[0] != all_same[1]);
CHECK(all_same.front() == all_same.back());

A card game

enum class colour { red, green, blue, yellow };
enum class value { number, draw_two, draw_four, reverse, skip };

struct card {
  colour colour;
  value value;
    
  friend bool operator==(card, card) = default;
};

A card game

auto const red_number = card{colour::red, value::number};
auto const blue_number = card{colour::blue, value::number};
auto const green_draw_two = card{colour::green, value::draw_two};
auto const blue_skip = card{colour::blue, value::skip};
auto const yellow_draw_four = card{colour::yellow, value::draw_four};

Stacks

// #include <stack>

auto deck = std::stack<card>();
REQUIRE(deck.empty());

deck.push(red_number);
deck.push(green_draw_two);
deck.push(green_draw_two);
deck.push(yellow_draw_four);
deck.push(blue_number);

REQUIRE(deck.size() == 5);

Removing elements from a stack

// #include <stack>

CHECK(deck.top() == blue_number);
deck.pop();

CHECK(deck.top() == yellow_draw_four);
deck.pop();

// ...

Comparing two stacks

auto const more_cards = deck;
REQUIRE(more_cards == deck);

deck.pop();
CHECK(more_cards != deck);

Queues

// #include <queue>
auto deck = std::queue<card>();
REQUIRE(deck.empty());

deck.push(red_number);
deck.push(green_draw_two);
deck.push(green_draw_two);
deck.push(yellow_draw_four);
deck.push(blue_number);

Removing elements from a queue

// #include <stack>

CHECK(deck.front() == red_number);
deck.pop();

CHECK(deck.front() == green_draw_two);
deck.pop();

// ...

Comparing two queues

auto const more_cards = deck;
REQUIRE(more_cards == deck);

deck.pop();
CHECK(more_cards != deck);

A range is an ordered sequence of elements with a designated start and rule for finishing

std::vector<std::string>{"Hello", "world!"}
std::string("Hello, world!")

ℕ    ℤ⁺    ℚ⁺    ℝ⁺

Exercise: how can be made into a range?

for (auto i = 0; std::cin >> i;) { ... }

Find revisited

unsigned int find(std::vector<int> const& v, int const value) {
    auto index = 0U;

    for (auto const i : v) {
        if (i == value) {
            return index;
        }
        
        ++index;
    }
    
    return index;
}

What type should the index become?

// Note: ??? is not a valid C++ symbol
??? find(std::vector<int> const& v, int const value) {
    auto index = ???;

    for (auto const i : v) {
        if (i == value) {
            return index;
        }
        
        ++index;
    }
    
    return index;
}

A Java linked list

class Node {
    Node next;
    public int value;
}

public class LinkedList {
    private Node head;
    
    public Node find(final int value) {
        Node i = head;
        while (i != null) {
            if (i.value == value) {
                return i;
            }
            i = i.next;
        }
        
        return null;
    }
}

Compare the pair

public Node find(final int value) {
    Node i = head;
    while (i != null) {
        if (i.value == value) {
            return i;
        }
        i = i.next;
    }
    return null;
}
unsigned int find(std::vector<int> const& v, int const value) {
    auto index = 0U;
    for (auto const i : v) {
        if (i == value) {
            return index;
        }
        ++index;
    }
    return index;
}

Could you imagine having to do that for...

...a doubly-linked list?

...a static vector?

...a double-ended queue?

...std::string?

...a skip list?

...a cord?

𝑥 sequence containers

𝑦 sequence algorithms (e.g. find)

×

𝑥𝑦 algorithm implementations

7 sequence containers

110 sequence algorithms (e.g. find)

×

770 algorithm implementations

and we haven't come close to exhausting either group

We need an intermediate representation

We need an intermediate representation

vector

Iterator

Algorithm

Result

Result sink

static vector

string

cord

skip list

linked list

The result may be a one or more iterators, a scalar value, or some combination of both.

𝑥 sequence containers

𝑦 sequence algorithms (e.g. find)

+

𝑥 + 𝑦 total implementations

7 sequence containers

110 sequence algorithms (e.g. find)

+

117 total implementations

and we haven't come close to exhausting either group

Operation

Array-like

Node-based

Iterator

Iterators: our intermediate representation

Iteration type

unsigned int
node*
unspecified

Read element

v[i]
i->value
*i
++i

Successor

i = i->next
++i

Advance fwd

j = i + n < v.size()
  ? i + n
  : v.size();
std::ranges::next(i, s, n)
j = i->successor(n)

Advance back

--i
i = i->prev
--i

Comparison

i < v.size()
i != nullptr
i != s

Predecessor

j = i - n < 0 ? 0 : i - n
std::ranges::prev(i, s, n)
j = i->predecessor(n)

Find revisited (again)

std::vector<int>::iterator
find(std::vector<int> const& v, int const value) {
    for (auto i = v.begin(); i != v.end(); ++i) {
        if (*i == value) {
            return i;
        }
    }
    return v.end();
}

Generating a hand of cards

auto hand = std::vector<card>{
   red_number,
   blue_number,
   green_draw_two,
   blue_number,
   blue_skip,
   yellow_draw_four,
   blue_number,
   blue_number,
   blue_skip,
};
// #include <algorithm>

CHECK(std::ranges::count(hand, red_number) == 1);
CHECK(std::ranges::count(hand, blue_number) == 4);
CHECK(std::ranges::count(hand, blue_skip) == 2);
// #include <algorithm>

auto card_to_play = std::ranges::find(hand, blue_number);
REQUIRE(card_to_play != hand.cend());
CHECK(*card_to_play == blue_number);

What is card_to_play?

Red number

Blue number

Green draw 2

Blue number

Blue skip

Yellow draw 4

Blue number

Blue number

Blue skip

Position: 1

Value: blue_number

Finding a card

// #include <algorithm>

auto const green_draw_four = card{colour::green, value::draw_four};
auto card_to_play = std::ranges::find(hand, green_draw_four);

REQUIRE(card_to_play == hand.cend());

What is card_to_play?

Red number

Blue number

Green draw 2

Blue number

Blue skip

Yellow draw 4

Blue number

Blue number

Blue skip

Position: 10

Value: n/a

// #include <algorithm>

auto card_to_play = std::ranges::find(hand, blue_number);
REQUIRE(card_to_play != hand.cend());
CHECK(*card_to_play == blue_number);

card_to_play = hand.erase(card_to_play);
REQUIRE(card_to_play != hand.cend());
CHECK(*card_to_play == green_draw_two);

What is card_to_play?

Red number

Green draw 2

Blue number

Blue skip

Yellow draw 4

Blue number

Blue number

Blue skip

Position: 1

Value: green_draw_two

Adding elements and iterators

// #include <algorithm>

auto card_to_play = std::ranges::find(hand, blue_number);
REQUIRE(card_to_play != hand.cend());
CHECK(*card_to_play == blue_number);

hand.push_back(green_draw_two);

What is card_to_play?

Red number

Blue number

Green draw 2

Blue number

Blue skip

Yellow draw 4

Blue number

Blue number

Blue skip

Position: 1

Value: blue_number

What is card_to_play?

Red number

Green draw 2

Blue number

Blue skip

Yellow draw 4

Blue number

Blue number

Blue skip

Position: 1

Value: blue_number

Blue number

Green draw 2

What is card_to_play?

// #include <algorithm>

auto card_to_play = std::ranges::find(hand, blue_number);
REQUIRE(card_to_play != hand.cend());
CHECK(*card_to_play == blue_number);

hand.push_back(green_draw_two);

card_to_play = std::ranges::find(hand, blue_number);
REQUIRE(card_to_play != hand.cend());
CHECK(*card_to_play == blue_number);

What is card_to_play?

Red number

Green draw 2

Blue number

Blue skip

Yellow draw 4

Blue number

Blue number

Blue skip

Position: 1

Value: blue_number

Blue number

Green draw 2

// #include <algorithm>

auto card_to_play = std::ranges::adjacent_find(hand, blue_number);
REQUIRE(card_to_play != hand.cend());
CHECK(*card_to_play == blue_number);

Red number

Blue number

Green draw 2

Blue number

Blue skip

Yellow draw 4

Blue number

Blue number

Blue skip

Position: 6

Value: blue_number

How many blue cards are there?

// #include <algorithm>

auto const blue_cards = std::ranges::count_if(hand, [](card const c) {
    return c.colour == colour::blue;
});

auto const expected_blue_cards = 6;
CHECK(blue_cards == expected_blue_cards);

Red number

Blue number

Green draw 2

Blue number

Blue skip

Yellow draw 4

Blue number

Blue number

Blue skip

Lambda unary predicate

[](card const c) {
    return c.colour == colour::blue;
}

Explicit return type

[](card const c) -> bool {
    return c.colour == colour::blue;
}

We'll need this to do a binary search

#include <compare>

enum class colour { red, green, blue, yellow };
enum class value { number, draw_two, draw_four, reverse, skip };

struct card {
  colour colour;
  value value;
    
  friend bool operator==(card, card)  = default;
  friend auto operator<=>(card, card) = default;
};
// #include <algorithm>

std::ranges::sort(hand);
REQUIRE(std::ranges::is_sorted(hand));

auto [first, last] = std::ranges::equal_range(hand, blue_number);
REQUIRE(first != last);
CHECK(std::ranges::distance(first, last) == 4);

CHECK(std::ranges::all_of(first, last, [blue_number](card const x) {
    return x == blue_number;
}));

Binary search

Red number

Blue number

Green draw 2

Blue number

Blue skip

Yellow draw 4

Blue number

Blue number

Blue skip

Position: 2

Value: blue_skip

Position: 9

Value: n/a

Lambda with value capture

[blue_number](card const x) {
    return x == blue_number;
}

Closures

auto const blue_then_yellow = [](card const x, card const y) {
    return x.colour == colour::blue and y.colour == colour::yellow;
};

auto const blue_card = std::ranges::adjacent_find(hand, blue_then_yellow);
REQUIRE(blue_card != hand.end());
CHECK(*blue_card == blue_skip);

auto const yellow_card = std::ranges::next(blue_card);
CHECK(*yellow_card == yellow_draw_four);

Let's take a note of how many swaps we do

void note_swaps(std::map<card, int>& cards_swapped,
                card const c) {
	auto result = cards_swapped.find(c);
	if (result == cards_swapped.end()) {
		cards_swapped.emplace(c, 1);
		return;
	}

	++result->second;
}

With house rules

// #include <algorithm>

// house rule: two players can swap a card of the same value
// (but for a different colour)
auto cards_swapped = std::map<card, int>();
std::ranges::transform(hand, hand.begin(), [&cards_swapped](card const c) {
    if (c.colour != colour::blue) {
        return c;
    }

    note_swaps(cards_swapped, c);
    return card{colour::green, c.value};
});

CHECK(std::ranges::none_of(hand, [](card const c) {
    return c.colour == colour::blue;
}));

Capturing by reference

[&cards_swapped](card const c) {
    // ...
}

Finishing off the example

{
    REQUIRE(cards_swapped.contains(blue_number));
    CHECK(cards_swapped.at(blue_number) == 4);
    auto const green_number = card{colour::green, value::number};
    CHECK(std::ranges::count(hand, green_number) == 4);
}
{
    REQUIRE(cards_swapped.contains(blue_skip));
    CHECK(cards_swapped.at(blue_skip) == 2);
    auto const green_skip = card{colour::green, value::skip};
    CHECK(std::ranges::count(hand, green_skip) == 2);
}
[](auto const& x, auto const& y) {
    return x == y;
}
// #include <functional>
std::ranges::equal_to()
// #include <functional>
std::plus()
[](auto const& x, auto const& y) {
    return x + y;
}

Constructing a vector of one type from a vector of another type

std::vector<double> standard_deviation_distribution();
static_cast<std::vector<int>>(standard_deviation_distribution());

Compile-time error: can't construct a vector<int> from a vector<double>

Constructing a vector of one type from a vector of another type

std::vector<double> standard_deviation_distribution();
auto const intermediate = standard_deviation_distribution();

std::vector<int>(intermediate.begin(), intermediate.end());

Captures vs parameters

[blue_number](card const x) {
    return x == blue_number;
}
auto const first_ten = std::vector<int>{
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
};
auto const first_hundred = std::vector<int>{
    0, 1, 2, 3, /* ... */, 99,
};
auto const first_thousand = std::vector<int>{
    0, 1, 2, 3, /* ... */, 999,
};

Generating a sequence of integers on demand

// #include <numeric>
auto first_10k = std::vector<int>(10'000);

// populates vector with values [0, 10'000)
// all values exist at once
std::iota(first_10k.begin(), first_10k.end(), 0);
// #include <ranges>

auto first_10k = std::views::iota(0, 10'000);
// can be used like a vector
// only "current" int exists at any given time
// like `range(100)` in Python 3

All algorithms have an overload where they take a begin and end pair, but only the std::ranges ones have range-based overloads too.

// #include <ranges>
namespace views = stdv;

auto is_blue = [](card const c) { return c.colour == colour::blue; };
auto all_blue = hand | stdv::filter(is_blue);

auto const expected = std::vector<card>{
    blue_number,
    blue_number,
    blue_skip,
    blue_number,
    blue_number,
    blue_skip,
};

CHECK(std::ranges::equal(expected, actual));
// #include <ranges>


auto const is_blue_card = [](card const c) { return c.colour == colour::blue; };
{
    auto const result = std::ranges::find_if(hand, is_blue_card);
    REQUIRE(result != hand.end());
    CHECK(*result == blue_number);
}

Reversing

// #include <ranges>
namespace stdv = std::views;

auto const is_blue_card = [](card const c) { return c.colour == colour::blue; };
{
    auto const result = std::ranges::find_if(hand, is_blue_card);
    REQUIRE(result != hand.end());
    CHECK(*result == blue_number);
}
{
    auto back_to_front = hand | stdv::reverse;
    
    auto const result = std::ranges::find_if(back_to_front, is_blue_card);
    REQUIRE(result != back_to_front.rend())
    CHECK(*result == blue_skip);
}
// #include <ranges>
namespace stdv = std::views;

auto const front3 = hand | stdv::take(3);
auto const expeceted std::vector<card>{
    red_number,
    blue_number,
    green_draw_two,
};
CHECK(std::ranges::equal(front3, expected));
// #include <ranges>
namespace stdv = std::views;

auto const back6 = hand | stdv::drop(3);
auto const expected = std::vector<card>{
    blue_number,
    blue_skip,
    yellow_draw_four,
    blue_number,
    blue_number,
    blue_skip,
};

CHECK(std::ranges::equal(back6, expected));

Operation

Array-like

Node-based

Iterator

Writeable iterators

Iteration type

unsigned int
node*
unspecified

Write

v[i]
i->value
*i
++i

Successor

i = i->next
++i

Advance

j = i + n < v.size()
  ? i + n
  : v.size();
std::ranges::next(i, s, n)
j = i->successor(n)

Comparison

i < v.size()
i != nullptr
i != s
// #include <algorithm>

void reset_scores(std::vector<int>& scores) {
    std::ranges::fill(scores, 0);
}
// #include <algorithm>

auto chars_to_words(std::vector<char> const& from,
                    std::string& to) {
    return std::ranges::copy(from, to.begin());
}

What happens when from.size() > to.size()?

Copying values from one range to another, existing range

H

e

l

o

t

h

e

r

e

l

From:

To:

Insert iterators

// #include <iterator>

auto to = std::vector<char>();
REQUIRE(to.empty());

ranges::copy(from, std::back_inserter(to));
CHECK(to == expected);

Works on containers with a push_back member function like vector's

Insert iterators

auto to = std::vector<char>(5);
REQUIRE(from.size() > to.size());
REQUIRE(not to.empty());

to.assign(from.begin(), from.end());
CHECK(to == expected);

Insert iterators

Works on containers that have push_back
(e.g. std::vector, std::string)

Works on containers that have push_front
(e.g. std::deque, std::list)

Works on containers that have insert
(e.g. all the above, std::unordered_set/map)

auto some_numbers = std::vector<int>{0, 1, 2, 7, 8, 9};
auto const missing = std::vector<int>{3, 4, 5, 6};

auto non_uniform_gap = std::ranges::adjacent_find(some_numbers,
    [](int const x, int const y) { return y - x != 1; });

some_numbers.insert(std::ranges::next(non_uniform_gap),
                    missing.begin(),
                    missing.end());

Could we have used iota?

auto some_numbers = std::vector<int>{0, 1, 2, 7, 8, 9};
auto const missing = stdv::iota(3, 7);

auto non_uniform_gap = std::ranges::adjacent_find(some_numbers,
    [](int const x, int const y) { return y - x != 1; });

some_numbers.insert(std::ranges::next(non_uniform_gap),
                    missing.begin(),
                    missing.end());

This won't work because vector::insert expects begin()and end()to return the same type, but missing's begin() and end() return different types.

views::common gives us a "common range"

auto some_numbers = std::vector<int>{0, 1, 2, 7, 8, 9};
auto const missing = stdv::iota(3, 7)
                   | stdv::common;

auto non_uniform_gap = std::ranges::adjacent_find(some_numbers,
    [](int const x, int const y) { return y - x != 1; });

some_numbers.insert(std::ranges::next(non_uniform_gap),
                    missing.begin(),
                    missing.end());

std::views::common adapts the previous slide's missing into a type whose begin() and end() member functions return the same type!

If the interface is in namespace std::ranges or std::views, then you don't need to pipe your view to stdv::common. Check the documentation if the interface is in namespace std.

Mutable iterators are iterators with both a read operation and a write operation.

What's going on here?

auto from = std::vector<int>(10);
auto to = std::vector<int>(10);

// ...

// We would use ranges::copy IRL
for (auto i = from.begin(), j = to.begin();
     i != from.end() and j != to.end(); ++i, ++j)
{
    *i = *j;
}

i is the read iterator, not the write one!

Iterator kinds

// mutable iterator       (similar to `T&`)
std::vector<T>::iterator

// read-only iterator     (similar to `T const&`)
std::vector<T>::const_iterator

Let T be the placeholder for any type.

Constant containers only have const_iterator

Getting a const_iterator from a mutable vector

auto from = std::vector<int>(10);
auto to = std::vector<int>(10);

// ...

// We would use ranges::copy IRL
auto i = from.cbegin();
for (auto j = to.begin();
     i != from.cend() and j != to.end(); ++i, ++j)
    *i = *j; // compile-time error: can't write to a const_iterator
}
Made with Slides.com