# Week 3 Solutions Doc
## Research Programming with C++
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://).
Open the folder in VSCode and build the dev container. This is the folder you should have open in VSCode when doing all the exercises.
## 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.
- It 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 multiple(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.
#### Solution:
A fraction consists of two pieces of information: a numerator and a denominator. So all we need to do to represent a fraction as data in a class is to store two integers, one to represent the numerator and one to represent the denominator (and be able to know which is which!). We can then implement our member functions by making use of these internal variables.
`Fraction.h`
```cpp=
#pragma once
#include <string>
class Fraction
{
public:
Fraction(int a, int b);
Fraction multiply(int a);
std::string toString();
double toDouble();
Fraction reciprocal();
private:
void simplify();
int numerator;
int denominator;
};
```
- The numerator and denominator can be public or private in this case, depending on your choice!
`Fraction.cpp`
```cpp=
#include "Fraction.h"
#include "FractionException.h"
#include <string>
Fraction::Fraction(int a, int b) : numerator(a), denominator(b) {}
std::string Fraction::toString()
{
return numerator == 0 ? "0" : std::to_string(numerator) + "/" + std::to_string(denominator);
}
double Fraction::toDouble()
{
return (double) numerator / denominator;
}
Fraction Fraction::reciprocal()
{
return Fraction(denominator, numerator);
}
Fraction Fraction::multiply(int a)
{
return Fraction(a*numerator, denominator);
}
```
#### 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?
#### Solution
<details>
The kinds of tests that you can do will be impacted by whether `numerator` and `denominator` are declared public or private / protected. If they are public, we can probe them directly e.g.:
```cpp=
TEST_CASE( "Check reciprocals", "[ex1]")
{
auto three_over_two = Fraction(2, 3).reciprocal();
REQUIRE(three_over_two.numerator == 3);
REQUIRE(three_over_two.denominator == 2);
}
```
Otherwise, we can only test them indirectly by checking what effect is had on observable behaviour. For example, we can convert to a string / double and that the value is correct:
```cpp=
TEST_CASE( "Check reciprocals", "[ex1]")
{
auto three_over_two = Fraction(2,3).reciprocal();
REQUIRE(three_over_two.toString() == "3/2");
REQUIRE(three_over_two.toDouble() == Approx(1.5).epsilon(1e-10));
}
```
This means that testing the correctness of the reciprocal is contingent on the correctness of the `toString` and `toDouble` functions. We have also had to check the knock-on effects to multiple functions because we need to check all the possible effects of this function.
We could handle this better if we can _overload_ the `==` operator. We'll learn about operator overloading next week, but we can define equality for objects of a given class. We could then write a test along the lines of:
```cpp=
TEST_CASE( "Check reciprocals", "[ex1]")
{
REQUIRE(Fraction(2,3).reciprocal() == Fraction(3,2));
}
```
which would check the equality of private values as well, and therefore give us a better confirmation of the correctness of our function in this case.
Another way to check the numerator and denominator directly would be to add `get` methods for both of these variables.
```cpp=
int getNumerator()
{
return numerator;
}
```
This would allow us to test the variables directly, but also allow other parts of the program to read them directly as well.
</details>
### Exercise 2: If you have time!
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 `StudentLaptop.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?
#### Solution:
<details>
Students should bear in mind that there are multiple ways of doing this exercise, and therefore you may do this in a way not covered by this solution. If you have done something in a different way, try to think about the pros and cons of both ways of doing it, and how that would inform your design decisions.
`Laptop.h`
```cpp=
#pragma once
#include <string>
class Laptop
{
public:
Laptop(int id, std::string os);
int ID;
std::string OS;
};
```
`Laptop.cpp`
```cpp=
#include "Laptop.h"
Laptop::Laptop(int id, std::string os) : ID(id), OS(os) {}
```
Laptop just needs to have two data fields and a constructor, it doesn't need to have any functions of its own.
We can have the `Student` class point to a `Laptop` in multiple ways. Here we will cover `unique_ptr` and `weak_ptr`.
We'll also treat the ability to give the laptop to another student as a member function in the `Student` class, because this will make the sample code more concise.
1. `unique_ptr`
`Student.h`
```cpp=
#pragma once
#include "Laptop.h"
#include <memory>
class Student
{
public:
Student(std::string name, int age);
void setLaptop(std::unique_ptr<Laptop> &laptop_ptr);
bool hasLaptop();
void transferLaptop(Student &other);
private:
std::unique_ptr<Laptop> laptop;
int age;
std::string name;
};
```
`Student.cpp`
```cpp=
#include "Student.h"
Student::Student(std::string name, int age) : name(name), age(age) {}
void Student::setLaptop(std::unique_ptr<Laptop> &laptop_ptr)
{
laptop = std::move(laptop_ptr);
}
bool Student::hasLaptop()
{
if (laptop)
{
return true;
}
else
{
return false;
}
}
void Student::transferLaptop(Student &other)
{
if (!other.hasLaptop())
{
other.setLaptop(laptop);
laptop.reset();
}
}
```
We need a method to set a student's laptop pointer (`setLaptop`). Since it's a `unique_ptr`, this needs to happen using move semantics (std::move). In this case we are passing in by reference and then applying the move inside the function. This leaves the original pointer empty, which people should be aware of. If you want to make this more explicit, you can pass by value instead of by reference, which forces any call made to the function to write `std::move(...)` inside the function arguments so it is clear that the pointer is losing control of the data.
We only want to transfer a laptop to a student who doesn't have one, so we have a `hasLaptop()` function which uses the `if(pointer)` syntax to check if the student's unique pointer is pointing to a laptop or not. `transferLaptop` does this check, and then if the other student doesn't have a laptop, transfers ownership of the laptop to the other student using `setLaptop` which, as you recall, makes a call to `std::move`, leaving the original student's laptop pointer empty.
**N.B.** `transferLaptop` takes the other student _by reference_ (`Student &other`) because it is making changes to that object what we want to persist!
2. Weak pointers
Weak pointers would, in principle, allow multiple students to point to a given laptop. It also allows that the `Laptop` object could continue to exist after the `Student` object without invoking explicit move operations.
`Student.h`
```cpp=
#pragma once
#include "Laptop.h"
#include <memory>
class Student
{
public:
Student(std::string name, int age);
void setLaptop(std::shared_ptr<Laptop> laptop_ptr);
bool hasLaptop();
void transferLaptop(Student &other);
private:
std::weak_ptr<Laptop> laptop;
int age;
std::string name;
};
```
`setLaptop` now takes a shared pointer, since weak pointers must always point to data owned by some shared pointer.
```cpp=
#include "Student.h"
Student::Student(std::string name, int age) : name(name), age(age) {}
void Student::setLaptop(std::shared_ptr<Laptop> laptop_ptr)
{
laptop = laptop_ptr;
}
bool Student::hasLaptop()
{
if (!laptop.expired())
{
return true;
}
else
{
return false;
}
}
void Student::transferLaptop(Student &other)
{
if (!other.hasLaptop())
{
other.setLaptop(laptop.lock());
laptop.reset();
}
}
```
This is similar to the unique pointer case except:
- We can use `.expired()` to check if the data is still valid.
- We don't need to use `move` because neither weak pointer owns the data.
- We need to `reset()` the original student's pointer so that it no longer points at the laptop that they have given away.
Some code to check that your functions are working might look like this:
```cpp=
#include "Laptop.h"
#include "Student.h"
#include <iostream>
int main()
{
std::shared_ptr<Laptop> someMac = std::make_shared<Laptop>(1, "OSX");
Student Alice("Alice", 22);
Student Bob("Bob", 19);
Alice.setLaptop(someMac);
std::cout << "Alice does " << (Alice.hasLaptop() ? "" : "not ") << "have a laptop" << std::endl;
std::cout << "Bob does " << (Bob.hasLaptop() ? "" : "not ") << "have a laptop" << std::endl;
Alice.transferLaptop(Bob);
std::cout << "Alice does " << (Alice.hasLaptop() ? "" : "not ") << "have a laptop" << std::endl;
std::cout << "Bob does " << (Bob.hasLaptop() ? "" : "not ") << "have a laptop" << std::endl;
return 0;
}
```
- If you are not familiar with the `... ? ... : ...` syntax, it is kind of branching logic that has three part: a boolean expression (followed by `?`) and then two expressions separated by `:`. The first expression is used if the boolean is true and the second expression is used if false.
- For example: `n % 2 == 0 ? "even" : "odd"` for evaluate to "even" for even `n` and "odd" for odd `n`.
- We can use this as part of other expressions like printing e.g. `std::cout << n % 2 == 0 ? "even" : "odd" << std::endl` will print to the console "even" or "odd" depending on if `n` is even or odd.
</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 program by typing `./build/bin/fractionAnalyser`
- When prompted you 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 [ex2]`. 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!
#### Solution:
<details>
Add the `stdexcept` header and change the constructor in `Fraction.cpp` like so:
```cpp=
#include "Fraction.h"
#include <stdexcept>
#include <string>
Fraction::Fraction(int a, int b) : numerator(a), denominator(b)
{
if (denominator == 0)
{
throw std::logic_error("Denominator of a fraction cannot be zero.");
}
}
```
</details>
#### 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.
#### Solution:
<details>
Then we can add `try` and `catch` blocks:
```cpp=
#include "Fraction.h"
#include <stdexcept>
#include <iostream>
#include <vector>
#include <memory>
int main()
{
int a, b;
std::cout << "Please enter two numbers (numerator and denominator) separated by a space" << std::endl;
bool input_okay = false;
std::unique_ptr<Fraction> frac;
std::cin >> a;
std::cin >> b;
try
{
frac = std::make_unique<Fraction>(a, b);
}
catch(std::logic_error e)
{
std::cout << e.what() << std::endl;
std::cout << "Adopting default fraction = 1/2." << std::endl;
frac = std::make_unique<Fraction>(1, 2);
}
std::cout << frac->toString() << " = " << frac->toDouble() << std::endl;
return 0;
}
```
Here we've changed `frac` to a pointer so that we can isolate the construction of the object from the handle that we use to access it.
</details>
#### Part 3
- Define your own exception class which derives from `logic_error`.
- Modify your `Fraction` constructor to throw this kind of exception instead.
- Modify your original catch block to catch your custom exception.
- Add an additional catch block to catch any other kinds of exceptions.
#### Solution:
<details>
`FractionException.h`
```cpp=
#include <stdexcept>
class FractionException : public std::logic_error
{
public:
FractionException(std::string);
};
```
`FractionException.cpp`
```cpp=
#include "FractionException.h"
#include <string>
FractionException::FractionException(std::string s) : std::logic_error(s) {}
```
We just need to define the constructor to call the base constructor with the same string.
We don't need `#include <stdexcept>` in `Fraction.cpp` to access `std::logic_error` because the include statement is already inside `FractionException.h` and therefore has already been included by the `#include "FractionException.h` statement.
</details>
#### Part 4: If you have time!
- 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.
#### Solution:
<details>
`FractionAnalyser.cpp`
```cpp=
#include "Fraction.h"
#include "FractionException.h"
#include <iostream>
#include <vector>
#include <memory>
int main()
{
int a, b;
std::cout << "Please enter two numbers (numerator and denominator) separated by a space" << std::endl;
bool input_okay = false;
std::unique_ptr<Fraction> frac;
do
{
try
{
std::cin >> a;
std::cin >> b;
frac = std::make_unique<Fraction>(a,b);
}
catch(FractionException e)
{
std::cout << e.what() << std::endl;
continue;
}
catch(...)
{
std::cout << "Unknown error occurred, terminating program." << std::endl;
return 1;
}
break;
}while(true);
std::cout << frac->toString() << " = " << frac->toDouble() << std::endl;
return 0;
}
```
In order to continually check the inputs until we get a valid one, we need to place the try/catch block inside a loop. The loop is broken when we get a valid pair. If some other error occurs, not due to invalid input, we go to the second catch block and we terminate the program with an error code (traditionally returning anything other than `0` from `main` indicates an error of some kind occurred).
</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.
The model that we want to implement is:
- 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!
#### Solution:
`Animal` should be an **abstract class**, and `void Speak()` should be a **pure virtual method**. This is because:
1. `Speak()` needs to be overridden by all derived classes.
2. `Speak()` is undefined for a generic `Animal` since all animals make different sounds. It would not reflect what we are modelling to give a sound to this class.
The other classes -- `Dog`, `Cat`, and `Budgie` -- should all inherit from `Animal`, and should use public inheritance. This will keep the `Speak()` function public in the derived classes as well.
We don't need to define a constructor explicitly because there is no data held by this example, so C++ will just create a default constructor.
`animalClasses.h`
```cpp=
#pragma once
#include <iostream>
class Animal
{
public:
virtual void Speak() = 0;
};
class Dog : public Animal
{
public:
void Speak();
};
class Cat : public Animal
{
public:
void Speak();
};
class Budgie : public Animal
{
public:
void Speak();
};
```
`animalClasses.cpp`
```cpp=
#include "animalClasses.h"
void Dog::Speak()
{
std::cout << "Woof" << std::endl;
}
void Cat::Speak()
{
std::cout << "Meow" << std::endl;
}
void Budgie::Speak()
{
std::cout << "Tweet" << std::endl;
}
```
#### 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`
#### Solution:
We'll show this example using unique pointers (because it is the most complicated to write!), but you can use whatever kind of pointer you want to check that this is working.
```cpp=
#include <iostream>
#include <vector>
#include "animalClasses.h"
#include <memory>
int main()
{
//declare a vector of animals here
std::vector<std::unique_ptr<Animal>> animals;
//put some different kinds of animals (dogs, cats, budgies) in it
animals.push_back(std::move(std::make_unique<Dog>()));
animals.push_back(std::move(std::make_unique<Cat>()));
animals.push_back(std::move(std::make_unique<Budgie>()));
//loop through all the animals in your vector and call Speak() on them
for(auto &a : animals)
{
a->Speak();
}
return 0;
}
```
#### 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`.
#### Solution:
In `animalClasses.h` add:
```cpp=
class Labrador : public Dog {};
class Terrier : public Dog
{
void Speak();
};
```
In `animalClasses.cpp` add:
```cpp=
void Terrier::Speak()
{
std::cout << "Yap" << std::endl;
}
```
Notice that we only have to re-define `Speak()` for the `Terrier` class. We have kept the default `Dog` behaviour for the `Labrador` class!
In `animalsSpeak.cpp`:
```cpp=
int main()
{
//declare a vector of animals here
std::vector<std::unique_ptr<Animal>> animals;
//put some different kinds of animals (dogs, cats, budgies) in it
animals.push_back(std::move(std::make_unique<Dog>()));
animals.push_back(std::move(std::make_unique<Cat>()));
animals.push_back(std::move(std::make_unique<Budgie>()));
animals.push_back(std::move(std::make_unique<Labrador>()));
animals.push_back(std::move(std::make_unique<Terrier>()));
//loop through all the animals in your vector and call Speak() on them
for(auto &a : animals)
{
a->Speak();
}
return 0;
}
```
#### Part 4
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?
#### Solution:
`Mammal`, `Bird`, etc. inherit from `Animal`. Then `Dog` and `Cat` inherit from `Mammal` and `Budgie` from `Bird`. `Dog`, `Cat`, and `Budgie` shouldn't inherit from `Animal` directly, they inherit from `Animal` _through_ `Mammal` or `Bird`.