Monday, 24 June 2024

Learn C++ by Example: Chapter 7

I have been sharing some details about my latest book "Learn C++ by Example", and gave an overview of chapter 6 last time.



You can buy my book directly here: http://mng.bz/AdAQ - or just go look at the table of contents. You can also buy it from Amazon: https://amzn.to/4dMJ0aG 

Chapter 7 is mainly about std::map and has a short section on loading a file. We build a game of "answer smash" which gives two clues to words that contain common letters at the start of one and end of the other. This means they can be overlapped. For example, a vector is a “sequential container supporting dynamic resizing,” and a torch could be defined as a “lit stick carried in one’s hand,” so smashing together the words vector and torch
[vector]  [torch] ->  [vec][tor][ch] -> vectorch
gives the answer vectorch.

Now, as you  probably know, std::map has been in C++ for a long time. In fact, C++11 introduced an unordered map, so why spend a chapter looking back rather than forwards? A reminder about some basics, like what the operator[] does (hint - it's not const and might do two things) are useful, and we'll explore a std::unordered_map in the next chapter. 

Using a map still helps us learn some new features. Many operations use a std::pair<const Key, T>, which can mean your code has 
it->first 
or 
it->second 
dotted around. Since C++17, we can use structured bindings to bind such pairs directly using more helpful names, for example:
for (const auto & [key, value] : dictionary)

We can bind to std::tuple and more besides. We can bind to arrays and even a structure’s non-static members too. For example, given

struct DataObject { int x{ 0 }; double y{ 1.23 }; };

we can write

DataObject data {};

auto [x, y] = data;

The chapter briefly looks at std::string_view, as a potentially more efficient way to concatenate strings. You need to be careful with lifetimes if you use this though. CppReference says 
It is the programmer's responsibility to ensure that std::string_view does not outlive the pointed-to character array


The chapter also includes a brief overview of big-O (order) notation, to help us think through potential inefficiencies, and this helps as a basis for unordered (hash based) lookup table in the next chapter. 

We also use a std::multimap, to allow duplicate values per key.  This allows us to load a dictionary from a file. There are several free dictionaries on the internet. Take your pick, but be warned my book only handles ASCII - other character sets would need a whole book, or at least a dedicated chapter or two! Loading a file doesn't need much work, once you've got the hang of streams. An input file stream is called std::ifstream, We can open a file, using its name, which may need to be fully pathed:

std::ifstream infile{ filename };

and use the stream in a Boolean context to see whether it is open:

if (infile)

// all good

The stream closes as it goes out of scope, which is sensible. 

To make a proper game, we need some randomness, so use std::sample to sample a few words. The challenging part is picking overlapping words.

So, some questions for you
  1. If we use operator[] for a std::map, say dictionary["word"], what are the two things that might happen?
  2. Can you remember how to find all the values for a given key in a std::multimap? (Clue: look up lower and upper bound)
  3. Have a go at finding words that can overlap a given word. (Maybe find a free dictionary so you can look up the words, and pick one at random to start with). 


Monday, 17 June 2024

Learn C++ by Example: Chapter 6

I have been sharing some details about my latest book "Learn C++ by Example", and gave an overview of the chapter 5 last time.



You can buy my book directly here: http://mng.bz/AdAQ - or just go look at the table of contents. You can also buy it from Amazon: https://amzn.to/4dMJ0aG 

Chapter 6 looks at smart pointers and dynamic polymorphism. We create a "Blob" class which moves forwards or possibly backwards. We end up with different Blob type, giving different possible movements, so can race the blobs.

The chapter starts with an abstract class, so we can write various derived classes for the race. The following might seem like a good starting point, but has several problems:

class Blob 

public:

    virtual void step() = 0;

    virtual int total_steps() const = 0; 

};

Did you spot any? 

Do we want to copy this class? Do we want a default constructor? It's worth being explicit about such things:

Blob() = default;

Blob(Blob const&) = delete;

Blob& operator=(Blob const&) = delete;

More importantly, a base class needs a virtual destructor.

virtual ~Blob() = default;

This has been the case in C++ since we've had classes, so probably isn't a surprise. Being able to say =default rather than {} might be though. C++11 introduced to ability to mark special member functions (and more besides) as defaulted or deleted.

We then look at the special member functions a class can have:

  • default constructor, X()
  • copy constructor, X(const X&)
  • copy assignment, operator = (const X&)
  • move constructor, X(X&&)
  • move assignment, operator = (X&&)
  • destructor, ~X()
Implementing one can block the others, and the table in Howard Hinnant's short blog is very useful if you can't recall what affects what.



The concrete classes move in various ways, for example a simple blob might move a fixed number of steps each time we call the step function. The book doesn't show how to use graphics libraries, so the reader can concentrate on learning newer C++ features, but if we use something like the SFML, we could display the marching blobs:


The book demonstrates how to use the console, with *s to represent a blob moving.

The book shows how to create other blobs using random distributions. This adds a bit more excitement, since you can't be certain which might win.

If we make a vector of blobs we need to use smart pointers, so that we can get the polymorphism we need. We can't make a std::vector<Blog> because blob is abstract. Even if it were not, we would slice derived classes, which is a bad thing.

In order to achieve polymorphism, we use a std::vector<std::unique_ptr<Blob>>. To add blobs, for example a StepperBlob which just marches at a set pace, we emplace a unique_ptr as follows:
blobs.emplace_back(std::make_unique<StepperBlob>());
We can add any other derived types too. 

Smart pointers are so much better than trying to handle raw pointers directly. The vector will call the destructor of each element when it goes out of scope, tidying up for us since a std::unique_ptr deletes the underlying raw pointer for us automatically. 

Some questions for you
1. Do you know which special member functions remain when you add a virtual destructor? (Cheat and look at Howard's table if you're not sure)
2. Can you list all the smart pointers in C++?
3. Which C++ distributions have you used? (If you would like some extra details, let me know and I'll write a short blog about these too).

Monday, 10 June 2024

Learn C++ by example: Chapter 5

I started to share some details about my latest book "Learn C++ by Example", and gave overviews of the first few chapters previously.



You can buy my book directly here: http://mng.bz/AdAQ - or just go look at the table of contents. You can also buy it from Amazon: https://amzn.to/4dMJ0aG

Chapter 5 is about arrays and objects. We create a card type and make a deck of cards using std::array. We use this to play a game of higher/lower. Simple, right? Yes, but an opportunity to learn several new C++  features.

 A card needs a suit and a value. We could use an int for each, but we would end up with a constructor taking two ints. Would we always remember which was which? If we use a scoped enum for the suit, we have a type, so the compiler will tell us if we make mistakes. We use the word class to make our new strongly typed enum:

enum class Suit {

    Hearts,

    Diamonds,

    Clubs,

    Spades

};

The chapter starts using this Suit and an int, but then introduces a class called FaceValue for the value. We use these in a Card class, and can make a constructor taking two different types:

Card(FaceValue value, Suit suit):

so we don't need to concentrate on which is which. We also look at non-static data members initialisation, allowing us to give default values to the Card's data members in place:

FaceValue value_{1};

Suit suit_{};

A deck of cards can then be an array:

std::array<Card, 52> deck;

The cards will then all use the default values, so we write a function to make 52 different cards. We can cycle around the suits using an initialiser list:

for (auto suit :

    {Suit::Hearts, Suit::Diamonds, Suit::Clubs, Suit::Spades})

and provide 13 of each with FaceValues from 1 to 13. The book also discusses various other approaches. 

In order to play higher/lower, we need to be able to compare cards, so we learn about the so-called spaceship operator or three-way comparison. Rather than hand writing operator<, or > or == for our Card type, we add

auto operator<=>(const FaceValue&) const = default;

to the FaceValue and 

auto operator<=>(const Card&) = default;

to the Card itself. The return type is auto, because types can have strong, weak or partial ordering. This would pull us into some rather cool mathematics, but at a high level, ordering numbers is easy enough, but some types, like a point in space are more difficult to agree on. Is (1, 3) greater then (4, 2) or not? The spaceship operator automagically compares all sub-objects recursively, which is why the contained FaceValue also needs a definition. This means a FaceValue of one is lowest. If one means an  ace, aces are low rather than high. You can have fun experimenting to make aces high instead if you want. 

If we shuffle the cards,  

std::random_device rd;

std::mt19937 gen{ rd() };

std::ranges::shuffle(deck, gen);

we can play the game. 

The book then shows how to add Jokers, by making a new struct:

struct Joker };

Nice and simple. We can then use a std::variant, containing a Joker or  Card

std::variant<Card, Joker>

and play the game again, letting a Joker give a free turn.

The book walks through details and alternatives for each of these steps. Here's some questions to think about

  1. Do you ever just use an int or other inbuilt type then get confused as to which parameter is which, or does you always use strong types?
  2. How would you generate the 52 cards? 
  3. Have a think about how to display the cards. When C++ has reflection, displaying the enum as a string will be easier. For now, we have to write some code ourselves.



Monday, 3 June 2024

Learn C++ by Example: chapter 4

I started to share some details about my latest book "Learn C++ by Example", and gave an overview of the chapter three last time last time.



You can buy my book directly here: http://mng.bz/AdAQ - or just go look at the table of contents. You can also buy it from Amazon: https://amzn.to/4dMJ0aG

Chapter 4 explores time points and durations and introduces literal suffixes. As with previous chapters, several other useful C++ features crop up too. We start by finding out the current date and time:

std::chrono::time_point now = std::chrono::system_clock::now();

This uses the chrono library introduced in C++11, and later in the chapter we use the calendrical types introduced in C++20. If you haven't met either of these features before, they feel very different to, say, using C's time_t. The chapter shows how to write countdowns to various dates, for example the end of a specific year or the end of the current year. 

For example, we can hard code a specific date, using C++20's year, month day, and transform it to time_point using sys_days:

auto new_years_eve = std::chrono::year_month_day(
    std::chrono::year(2022),
    std::chrono::month(12),
    std::chrono::day(31)
);
auto event = std::chrono::sys_days(new_years_eve);

If we subtract the current time, we obtain a duration:

std::chrono::duration dur = event - now;

That gives us our first countdown, but there are details behind the times, dates and durations. If we print out the duration, the result varies between compilers. Visual Studio 2022 gave

69579189669221[1/10000000]s

while clang and gcc both gives the result in "ns". This gives us a hint that durations are in units.

We can change hours to milliseconds with an implicit cast:

std::chrono::milliseconds ms = std::chrono::hours(2);

but going back to hours from a more fine grained duration requires an explicit cast:

auto two_hours_from_ms = duration_cast<hours>(ms);

The library tends to give compile errors if you get something wrong, which is a good thing. 

In order to fully understand how the duration casts work, we need to delve into std::ratio, which is used to define seconds, minutes and so on.

The chapter gives a brief introduction to requirements and concepts too, which is a big topic. At a high level, they provide constraints for template arguments, picking appropriate overloads or giving clearer compiler error messages. 

The chapter also covers literal suffixes, such as "Hello, world!"s to make a std::string rather than a char *. Chrono also provides operator""s, taking a number, so we can write 59s for 59 seconds. We could say seconds{59} instead, but in conjunction with the overloaded operator /, we can write dates clearly in code. For example, 

auto new_years_eve = 2022y / December / 31;

is hopefully obvious, even if you don't know C++ or the how the chrono library works.


So, some questions/challenges for you. 
  • Try to write various dates and times and find the durations between them.
  • How many seconds are there in a year? (Yes, a bit of a trick question!)
  • Write your own duration and corresponding literal operator.