# PHAS0100 - Week 3 (27th January 2023) ## Introduction to Modern C++ 2 This week we'll learn how to write classes, make use of inheritance, and handle errors using exceptions. ## Before You Begin **Please clone the exercise repository for this week and build your dev container before class.** You can [clone the repository here](https://github.com/UCL-PHAS0100-22-23/week3_cpp_exercises). Open the folder `week3_cpp_exercises`, which contains the `.devcontainer`, in VSCode and build the dev container. This is the folder you should have open in VSCode when doing all the exercises. You will move between the subfolders to compile individual exercises using the terminal in VSCode. **You can also work in the github codespaces for these exercises if your dev container is being unresponsive.** ## Timetable for this afternoon | Start | End | Topic | | -------- | -------- | -------- | | 14:00 | 14:10 | Introduction | | 14:10 | 14:40 | Section 1: Classes | | 14:40 | 14:50 | Section 1 Quiz & Discussion | | 14:50 | 15:00 | Break | | 15:00 | 15:10 | Walkthrough of Exception Documentation | | 15:10 | 15:40 | Section 2: Exceptions | | 15:40 | 15:50 | Break | | 15:50 | 16:00 | OOP Quiz and intro to Section 3 | | 16:00 | 16:40 | Section 3: Inhertance and Polymorphism | | 16:40 | 16:50 | Section 3 Quiz & Discussion | ## Section 1: Classes ### Exercise 1: Open a new terminal in VSCode. In the terminal type: ```bash= cd Fractions ``` to move into the `Fractions` folder. We'll keep our terminal in this folder for the remainder of the exercise, so you'll build and run your program and tests from here. #### Part 1 In the files `Fraction.h` (in `include`) and `Fraction.cpp` (in `src`), write a class called `Fraction` to model a fraction (i.e. rational number). It should be able to be constructed by passing the numerator and denominator to the constructor, and does not need to simplify the fraction (e.g. $\frac{2}{4}$ is okay; it doesn't need to convert it to $\frac{1}{2}$). - It should implement the following (public) functions: - `Fraction(int a, int b)` - Constructor for fraction $\frac{a}{b}$ - `Fraction reciprocal()` - Returns a new fraction which is the reciprocal of the current fraction e.g. the reciprocal of $\frac{2}{3}$ is $\frac{3}{2}$. - `Fraction multiply(int a)` - Returns a new fraction which is the current fraction multiplied by an integer. - `double toDouble()` - Calculates the value of the fraction as a double - `std::string toString()` - Generate a string to represent the fraction - If the numerator is `0` then the string should be `"0"` regardless of the denominator. - Otherwise should be in the form `"a/b"`. For example, a `Fraction` representing $\frac{1}{2}$ should return the string `"1/2"`. **HINT**: If you are not sure how a class should be written, you can look in the `exampleClass` folder. This contains the code for a class representing a sphere called `Sphere`. Note that the class declaration is in the header (.h) file (`Sphere.h` in `include`), and the definition of member functions in the source (.cpp) file (`Sphere.cpp` in `src`). Your code should separate your class declaration and implementation in the same way. #### Part 2 - Compile your code (executable and tests) using: ```bash= cmake -B build cmake --build build ``` - Run the tests using: ```bash= ./build/bin/test_ex [ex1] ``` Don't forget the `[ex1]` part of the command; this tells the test suite that we only want to run the tests associated with this exercise. There is an extra set of tests with the label `[ex3]` which won't pass yet, so we don't want to run all of the tests right now! - If any of the tests don't pass, you should check why and fix your code! - Go into the tests source file `test/src/test_fractions.cpp` - Fill in some tests for the `reciprocal` function. - Is this test impacted by your choice to make the numerator and denominator public or private? ### Exercise 2: If you have time! <details> This exercise explores _aggregation_ in a class: where one class points to an instance of another class. - In the terminal, change directories into the `Students` folder. - If your terminal are still in the `Fractions` folder, you can type `cd ../Students` - If your terminal is in the top level directory, you can type `cd Students` - In `Student.h` and `Student.cpp`, write a class to represent a `Student`, which has a `name` and `age`. - In `Laptop.h` and `Laptop.cpp`, write a class to represent a laptop, which has an `ID`, and `OperatingSystem`. - Add a member to your `Student` class that can point to a laptop. Make sure that this can also account for a student not having one. - You can decide which kind of pointer makes the the most sense to you for the class that you're writing! - Should the `Student` own the memory, or should the `Laptop` be an independently managed entity? This is subjective and would normally depend on additional context. - Write a function which allows you to set this pointer. - Write a function that allows one `Student` to give their laptop to another `Student` who does not have one. - Think about what should happen to the laptop pointer for each student when the is given from one to the other. - What happens to the ownership of the memory? - You should decide whether this function is a member function, or a function external to the class. - Use the `main` function in `StudentLaptopManagement.cpp` to test your classes. - Can you create two students? - Can you create a laptop? - Can you assign the laptop to one of the students? - Can you then transfer it to the other student? - Do you need to add any functions to help you check what's happening inside your classes? </details> ## Section 2: Exceptions ### Exercise 3: We'll next add some error handling to your `Fraction` class. A fraction cannot have `0` as a denominator, since division by `0` is undefined. We'll go through some of the exception types that we can use, and how to find them in the [reference documentation](https://cplusplus.com/reference/exception/exception/) during class. #### Part 1 - Move back into the `Fractions` folder if you're not there in your terminal. - Modify the constructor of the `Fraction` class to throw an [appropriate exception](https://cplusplus.com/reference/exception/exception/) if the denominator is `0`. - See the derived types of `exception` in the documentation, and select one which best applies. - Give the exception an appropriate error message when you throw it. - Try compiling and running your program (build the same was as for exercise 1), and check that your exception is thrown. - You can run the main executable by typing `./build/bin/fractionAnalyser` - When prompted to, enter two integers separated by a space, e.g. `2 3`, and then press enter. - Try entering a `1 0` to represent the fraction $\frac{1}{0}$. Does your exception get raised? What happens when it does? - Try running the test for exercise 2 using `./build/bin/test_ex [ex3]`. It should pass if you are throwing your exception correctly! - Run _all_ tests with the command `./build/bin/test_ex`; this will run both sets of tests to check that nothing you've done in this exercise has messed up your other functionality! #### Part 2 - In the `main` function in `FractionAnalyser.cpp`, we accept two integers from the user (using `cin`) and turn this into a `Fraction`, then print out some information about it. - Add `try` and `catch` blocks to `main` to catch the exception if the user asks the program to generate a fraction with denominator `0`. - In the catch block we should: - Print the error message from the exception. - Use a default fraction e.g. $\frac{1}{2}$. - Print to the console an explanation that you are using a default value. #### Part 3 - Define your own exception class which derives from `std::logic_error`. - You will need to declare a `public` constructor which takes an argument of type `std::string`. - When a derived class is constructed, the base class is constructed as well: see [_How is a Derived Object Created and Destroyed_](https://https://github-pages.ucl.ac.uk/research-computing-with-cpp/03cpp2/sec02Inheritance.html) in the notes for this week. - Make sure your constructor specifies that the base class should use a constructor taking the string argument. - Modify your `Fraction` constructor to throw this kind of exception instead. - Make sure that you have the headers that you need in `Fraction.cpp`! - Modify your original catch block to catch your custom exception. - Add an additional catch block to catch any other kinds of exceptions. #### Part 4: If you have time! <details> - Modify the code in `main` so that if you catch an exception thrown from the constructor due to division by `0`, you ask the user to enter another pair of numbers. - Remember that this pair of numbers will have to be treated the same way: if the user enters another bad pair then the exception should be caught and the user asked again and so on. </details> ## Section 3: Inheritance & Polymorphism ### Exercise 4: #### Part 1 In this exercise we'll be writing some classes from scratch and implementing a inheritance hierarchy. In your terminal, move into the `Animals` folder. We'll do all of the development for this exercise here. We'll be writing come classes in `animalClasses.h`/`animalClasses.cpp` and writing our `main` function in `animalsSpeak.cpp`. **Write some classes to implement the following model:** - We want a class `Animal` to represent animals. All animals can make a sound through the `void speak()` method, which prints the sound the animal makes to the console. - We can't know what sound an animal will make unless we know what type of animal it is. - There are three types of animals in our model: `Dog`, `Cat`, and `Budgie`. - Dogs say "Woof" - Cats say "Meow" - Budgies say "Tweet". - There should be separate classes for `Dog`, `Cat`, and `Budgie`. - You can write the declarations for all of these classes in `animalClasses.h`, and the definitions for their functions in `animalClasses.cpp`. This will make it easier to keep track of your code and the relationships between these classes compared to keeping them all in different files! #### Part 2 We'll now check that our polymorphism is working correctly. - In `main`, in the file `animalsSpeak.cpp`, create a vector of _pointers to animals_, and populate it with pointers to some animals of different types. Make sure that this vector has at least one of each kind of animal (`Dog`, `Cat`, `Budgie`) in it. - If you're using `std::unqiue_ptr<Animal>` in your vector, you will need to create each animal you want to put in your vector using `std::make_unique<Dog>()` (or `<Cat>` or `<Budgie>`) and use `std::move` inside `push_back` to move the ownership into the vector. - Loop through this vector and call `speak` for each animal. - Check that your program output is correct: each `Dog` should say `Woof`, each `Cat` should say `Meow` and each `Budgie` should say `Tweet`. - Compile with `cmake -B build`, then `cmake --build build` - Run with `./build/bin/animalsSpeak` #### Part 3 We can extend out inheritance tree to more than one level. Let's create some sub-types of dog: `Labrador` and `Terrier`. The Labrador should still say "Woof", but the Terrier should say "Yap". - Add a `Labrador` and a `Terrier` to your vector of animals. - Re-compile and re-run your code to check that they are also working correctly. - Because `Labrador` and `Terrier` derive from `Dog`, and `Dog` derives from `Animal`, `Labrador` and `Terrier` also derive from `Animal`. #### Part 4 In this final part, we'll look at a possible design decision we might have to make with the code we just developed. Animals can also belong to different biological classes. For example, dogs and cats are mammals, and budgies are birds. Suppose that we now want to have these biological groupings represented in our code as well. - How could we represent these relationships through inheritance? - Suppose we write new C++ classes for `Mammal` and `Bird`. - How would the `Mammal` and `Bird` classes be related to the `Animal`, `Dog`, `Cat`, and `Budgie` classes? - We'll collect answers in the quiz at the end of this session! Now let's consider an alternative: we don't always use inheritance to represent such relationships in C++! We can also represent the class of an animal not through inheritance, but through a property of the animal i.e. a class member variable. A classification like this could be represented by an `enum` (short for _enumeration_) in C++: ```cpp= enum BioClass { Mammal, Bird, Reptile, BonyFish }; ``` We can then use `BioClass` like a type, although actually each option in the `enum` is represented by an integer. We can add a member variable called `classification` of type `BioClass` to our `Animal` class, and override this member so that `Dog` and `Cat` have `classification` equal to `Mammal`, and `Budgie` has `classification` equal to `Bird.` Given these two approaches, which is better suited to the following cases: 1. We have a class `Aviary` where birds are kept. It has a member variable which is a `vector` keeping track of all the birds that it houses. We want to be sure that the only animals that can be in this list are birds. 2. We have a veterinary clinic. It has a list of animals in its care, stored as a `vector<Animal*>`. We want to be able to go through this list and assign a different specialist to each depending on whether the animal is a `Mammal`, `Bird`, `Reptile` etc. Some things you might want to consider are: - How does inheritance based polymorphism impact program safety? How does C++ type checking affect what kind of objects you can pass around? - Representing a property through polymorphism or a member variable can change where the responsibility lies to distinguish between different kinds of objects. - Polymorphism relies on virtual methods defined and overridden within those classes. Member functions called can then have distinguishing behaviour. - Inspecting a member variable and acting accordingly is the responsibility of an external function. An external function can read a member variable and change behaviour based on `if` statements, rather than placing those differences inside the class definitions themselves. - Are these approaches suited to different situations? Are there cases where it makes more sense for the logic to be placed internally or externally? **We'll have a quiz and discuss these at the end of the session!**