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.



No comments:

Post a Comment