COMP6771

Advanced C++ Programming

Week 2

Libraries

What's a library?

Why do we want to use libraries?

auto what_am_i(std::vector<int> const& v,
               int const x) -> int {
    for (auto i = 0; i <= ranges::distance(v); ++i) {
        if (v[i] == x) {
            return i;
        }
    }
    return ranges::distance(v);
}

Why do we want to use libraries?

auto find(std::vector<int> const& v,
          int const x) -> int {
    for (auto i = 0; i <= ranges::distance(v); ++i) {
        if (v[i] == x) {
            return i;
        }
    }
    return ranges::distance(v);
}

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
  • Abseil
  • Catch2 (test framework)
  • {fmt}
  • gsl-lite
  • range-v3

You are not expected to learn everything in all of 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(ranges::distance(v) == 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(ranges::distance(v) == 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(ranges::distance(v) == 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(ranges::distance(v) == 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(ranges::distance(v) == 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(ranges::distance(v) == 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
        }
    }
}

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.

We may explore these in greater detail in Week 10.

The Abseil flat-hash containers offer significant performance benefits over the std:: containers, which is why we use them in COMP6771.

For the purposes of COMP6771, they are interface-compatible.

Associative containers

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

A collection of unique keys.

A collection of keys.

Associative array that map a unique keys to values.

Associative array where one key may map to many values.

We may explore these in greater detail in Week 10.

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>

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

String concatenation

// #include <absl/strings/str_cat.h>
// #include <string>

auto const greeting = absl::StrCat("hello", "world", "!");

String formatting

// #include <fmt/format.h>
// #include <iostream>
// #include <string>

auto const message = fmt::format("The meaning of life is {}", 42);
std::cout << message << '\n';

String formatting

// #include <fmt/format.h>
// #include <iostream>
// #include <string>

auto const message = fmt::format("pi has the value {}", 3.1415);
std::cout << message << '\n';

String formatting

// #include <fmt/format.h>
// #include <iostream>
// #include <string>

auto const message = fmt::format("life={}, pi={}", 42, 3.1415);
std::cout << message << '\n';

String formatting

// #include <fmt/format.h>
// #include <iostream>
// #include <string>

auto const message = fmt::format("life={}, pi={}", 3.1415, 42);
std::cout << message << '\n';

Positional "named" args

// #include <fmt/format.h>
// #include <iostream>
// #include <string>

auto const message = fmt::format("life={life}, pi={pi}",
                                 fmt::arg("pi", 3.1415),
                                 fmt::arg("life", 42));
std::cout << message << '\n';

std::vector revisited

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

some_ints.push_back(42);
REQUIRE(ranges::distance(some_ints) == 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(ranges::distance(some_ints) == 6);

some_ints.pop_back();
REQUIRE(ranges::distance(some_ints) == 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(ranges::distance(some_ints) == 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(ranges::distance(some_ints) == 6);

some_ints.clear(); // removes *all* the elements
CHECK(some_ints.empty());

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(ranges::distance(all_default) == 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(ranges::distance(all_same) == 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 auto operator==(card, card) -> bool = 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

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

    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
auto 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;
}
auto find(std::vector<int> const& v, int const value) -> int {
    auto index = 0;
    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

gsl_lite::index
node*
unspecified

Read element

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

Successor

i = i->next
++i

Advance fwd

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

Advance back

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

Comparison

i < ranges::distance(v)
i != nullptr
i != s

Predecessor

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

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 <range/v3/algorithm.hpp>

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

auto card_to_play = 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 <range/v3/algorithm.hpp>

auto const green_draw_four = card{colour::green, value::draw_four};
auto card_to_play = 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 <range/v3/algorithm.hpp>

auto card_to_play = 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 <range/v3/algorithm.hpp>

auto card_to_play = 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 <range/v3/algorithm.hpp>

auto card_to_play = 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 = 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 <range/v3/algorithm.hpp>

auto card_to_play = 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 <range/v3/algorithm.hpp>

auto const blue_cards = 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 auto operator==(card, card) -> bool = default;
  friend auto operator<=>(card, card) = default;
};
// #include <range/v3/algorithm.hpp>

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

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

CHECK(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 = ranges::adjacent_find(hand, blue_then_yellow);
REQUIRE(blue_card != hand.end());
CHECK(*blue_card == blue_skip);

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

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

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

	++result->second;
}

With house rules

// #include <range/v3/algorithm.hpp>

// house rule: two players can swap a card of the same value
// (but for a different colour)
auto cards_swapped = std::map<card, int>{};
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(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(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(ranges::count(hand, green_skip) == 2);
}

Library function objects

[](auto const& x, auto const& y) {
    return x == y;
}
// #include <range/v3/functional.hpp>
ranges::equal_to{}

is roughly equivalent to

Library function objects

[](auto const& x, auto const& y) {
    return x != y;
}
// #include <range/v3/functional.hpp>
ranges::not_equal_to{}

is roughly equivalent to

Library function objects

[](auto const& x, auto const& y) {
    return x + y;
}
// #include <range/v3/functional.hpp>
ranges::plus{}

is roughly equivalent to

Library function objects

[](auto const& x, auto const& y) {
    return x * y;
}
// #include <range/v3/functional.hpp>
ranges::multiplies{}

is roughly equivalent to

ranges::distance vs vector::size

We usually want to use ranges::distance because its return type is implicitly compatible with int.

The vector/string interface uses a different type with different characteristics, and we don't want to mix them up. The compiler helps us with this.

You can use size for those parts of the interface, if you keep the scopes small.

// E.g. 1
auto v = std::vector<int>(other.size());

// E.g. 2 (yuck, but best option till you get more experience)
for (auto i = 0; i < ranges::distance(v); ++i) {
    using size_type = std::vector<int>::size_type; // C++ typedef
    v[gsl_lite::narrow_cast<size_type>(i)];
}

// E.g. 3 i should not leave the scope of the loop
for (auto i = std::vector<int>::size_type{0}; i < v.size(); ++i) {
    v[i];
}

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

auto standard_deviation_distribution() -> std::vector<double>;
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

auto standard_deviation_distribution() -> std::vector<double>;
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 <range/v3/numeric.hpp>
auto first_ten_thousand = std::vector<int>(10'000);

// populates vector with values [0, 10'000)
ranges::iota(first_ten_thousand, 0);

Generating a sequence of integers on demand

// #include <range/v3/range.hpp>
// #include <range/v3/view.hpp>

auto first_hundred = views::iota(0, 100);
auto const all_at_once = first_hundred
                       | ranges::to<std::vector>;

CHECK(ranges::equal(first_hundred, all_at_once));
// #include <range/v3/range.hpp>
// #include <range/v3/view.hpp>
namespace views = ranges::views;

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

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

auto const actual = all_blue | ranges::to<std::vector>;
CHECK(expected == actual);

Filters ("remove if")

// #include <range/v3/range.hpp>
// #include <range/v3/view.hpp>
namespace views = ranges::views;

auto is_blue = [](card const c) { return c.colour == colour::blue; };
auto no_blue = hand | views::remove_if(is_blue);

auto const expected = std::vector<card>{
    red_number,
    green_draw_two,
    yellow_draw_four,
};

auto const actual = no_blue | ranges::to<std::vector>;
CHECK(expected == actual);
// #include <range/v3/range.hpp>
// #include <range/v3/view.hpp>
namespace views = ranges::views;

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

Reversing

// #include <range/v3/range.hpp>
// #include <range/v3/view.hpp>
namespace views = ranges::views;

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

auto swap_blue = [](card const c) {
    return c.colour != colour::blue ? c : card{colour::green, c.value};
};

auto const expected = std::vector<card>{
    red_number,
    green_number,
    green_draw_two,
    green_number,
    green_skip,
    yellow_draw_four,
    green_number,
    green_number,
    green_skip,
};

auto const actual = hand | views::transform(swap_blue);
CHECK(expected == actual);

Splitting strings

// #include <range/v3/range.hpp>
// #include <range/v3/view.hpp>
namespace views = ranges::views;
using namespace std::string_literals;

auto const sentence = "the quick brown fox jumps over the lazy dog"s;
auto to_string = [](auto x) { return x | ranges::to<std::string>; };
auto const individual_words = sentence
                            | views::split(' ')
                            | views::transform(to_string)
                            | ranges::to<std::vector>;

auto const expected = std::vector<std::string>{
    "the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"
};

CHECK(individual_words == expected);

Joining strings

// #include <range/v3/range.hpp>
// #include <range/v3/view.hpp>
namespace views = ranges::views;

auto const individual_words = std::vector<std::string>{
    "the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"
};

auto const sentence = words | views::join | ranges::to<std::string>;

using namespace std::string_literals;
auto const expected = "thequickbrownfoxjumpsoverthelazydog"s;
CHECK(sentence == expected);

Joining strings

// #include <range/v3/range.hpp>
// #include <range/v3/view.hpp>
namespace views = ranges::views;

auto const individual_words = std::vector<std::string>{
    "the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"
};

auto const sentence = words | views::join(' ') | ranges::to<std::string>;

using namespace std::string_literals;
auto const expected = "the quick brown fox jumps over the lazy dog"s;
CHECK(sentence == expected);

Concatenating ranges

// #include <range/v3/range.hpp>
// #include <range/v3/view.hpp>
namespace views = ranges::views;

using namespace std::string_literals;
auto const first = "the quick brown "s;
auto const second = "fox jumps over"s;
auto const thrid = std::vector<std::string>{" the", "lazy", "dog"};

auto const sentence = views::concat(first, second, third | views::join(' '))
                    | ranges::to<std::string>;

auto const expected = "the quick brown fox jumps over the lazy dog"s;
CHECK(sentence == expected);
// #include <range/v3/range.hpp>
// #include <range/v3/view.hpp>
namespace views = ranges::views;

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

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

CHECK(back6 == expected);

Use only the last n elements

// #include <range/v3/range.hpp>
// #include <range/v3/view.hpp>
namespace views = ranges::views;

auto const back2 = hand | views::take_last(2) | ranges::to<std::vector>;
auto const expeceted std::vector<card>{
    blue_number,
    blue_skip,
};
CHECK(back2 == expected);

Don't use the last n elements

// #include <range/v3/range.hpp>
// #include <range/v3/view.hpp>
namespace views = ranges::views;

auto const front6 = hand | views::drop_last(2) | ranges::to<std::vector>;
auto const expeceted std::vector<card>{
    red_number,
    blue_number,
    green_draw_two,
    blue_number,
    blue_skip,
    yellow_draw_four,
    blue_number,
};
CHECK(front6 == expected);

Iterating over multiple ranges at once

// #include <range/v3/numeric.hpp>
// #include <range/v3/range.hpp>
// #include <range/v3/view.hpp>
namespace views = ranges::views;

auto hamming_distance(std::string const& s1,
                      std::string const& s2) -> int {
    auto different = views::zip_with(ranges::not_equal_to{}, s1, s2);
    return ranges::accumulate(different, 0);
}

CHECK(hamming_distance("chew", "chop") == 2);
CHECK(hamming_distance("hello", "world") == 4);

Operation

Array-like

Node-based

Iterator

Writeable iterators

Iteration type

gsl_lite::index
node*
unspecified

Write

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

Successor

i = i->next
++i

Advance

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

Comparison

i < ranges::distance(v)
i != nullptr
i != s
auto reset_scores(std::vector<int>& scores) -> void {
    ranges::fill(scores, 0);
}
auto chars_to_words(std::vector<char> const& from,
                    std::string& to) {
    return ranges::copy(from, to.begin());
}

What happens when ranges::distance(from) > ranges::distance(to)?

Copying values from one range to another, existing range

H

e

l

o

t

h

e

r

e

l

From:

To:

Insert iterators

// #include <range/v3/iterator.hpp>

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

ranges::copy(from, ranges::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(ranges::distance(from) > ranges::distance(to));
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, absl::flat_hash_set/map)

I want to insert elements into my vector!

auto some_numbers = views::concat(views::iota(0, 50), views::iota(75, 100))
                  | ranges::to<std::vector>;

auto square = [](int const x) { return x * x; };
auto more_numbers = views::iota(50, 75)
                  | views::transform(square)
                  | ranges::to<std::vector>;

auto non_uniform_gap = ranges::adjacent_find(some_numbers,
    [](int const x, int const y) { return y - x != 1; });
some_numbers.insert(ranges::next(non_uniform_gap),
                    more_numbers.begin(), more_numbers.end());

...but we didn't really need two vectors?

auto some_numbers = views::concat(views::iota(0, 50), views::iota(75, 100))
                  | ranges::to<std::vector>;

auto square = [](int const x) { return x * x; };
auto more_numbers = views::iota(50, 75) | views::transform(square);

auto non_uniform_gap = ranges::adjacent_find(some_numbers,
    [](int const x, int const y) { return y - x != 1; });
some_numbers.insert(ranges::next(non_uniform_gap),
                    more_numbers.begin(), more_numbers.end());

std::vector::insert wants a "common range"

auto some_numbers = views::concat(views::iota(0, 50), views::iota(75, 100))
                  | ranges::to<std::vector>;

auto square = [](int const x) { return x * x; };
auto more_numbers = views::iota(50, 75) | views::transform(square);

// This won't work because vector::insert expects begin and end to have
// the same type, but more_numbers' begin and end are different types.
auto non_uniform_gap = ranges::adjacent_find(some_numbers,
    [](int const x, int const y) { return y - x != 1; });
some_numbers.insert(ranges::next(non_uniform_gap),
                    more_numbers.begin(), more_numbers.end());

views::common gives us a "common range"

auto some_numbers = views::concat(views::iota(0, 50), views::iota(75, 100))
                  | ranges::to<std::vector>;

auto square = [](int const x) { return x * x; };
auto more_numbers = views::iota(50, 75)
                  | views::transform(square)
                  | views::common;

// views::common will adapt the previous slide's more_numbers'
// begin and end into a type that has a _common_ begin and end
// type (hence the name views::common).
auto non_uniform_gap = ranges::adjacent_find(some_numbers,
    [](int const x, int const y) { return y - x != 1; });
some_numbers.insert(ranges::next(non_uniform_gap),
                    more_numbers.begin(), more_numbers.end());

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
}

COMP6771 20T2 - 2.1 - C++ Libraries

By cs6771

COMP6771 20T2 - 2.1 - C++ Libraries

  • 1,995