This project has been created as part of the 42 curriculum by baelgadi.
Subtype polymorphism, abstract classes and interfaces
The Helmet of Nabu does not care whose head it rests on.
Kent Nelson, Khalid, Eric and Linda. Different mortals, one same call to power.
A base-class pointer is the Helmet. The wearer underneath decides what the magic actually does.
|
CPP Module 04:
4 exercises. C++98 standard only. Compiler: c++ · Flags: -Wall -Wextra -Werror · Must compile with -std=c++98Exercise 00: Polymorphism · ex00/ · Animal/Dog/Cat + the WrongAnimal contrastExercise 01: I don’t want to set the world on fire · ex01/ · Brain + deep copyExercise 02: Abstract class · ex02/ · Animal becomes pure virtualExercise 03: Interface & recap · ex03/ · AMateria, Character, MateriaSourceForbidden: using namespace (-42) · friend (-42) · printf · *alloc · free · STL containers · STL algorithms · function bodies in headersRequired: Orthodox Canonical Form on every non-interface class · a virtual destructor on every polymorphic base · specific construction/destruction messages |
When the Helmet speaks, it is never the Helmet’s voice. It is the wearer’s.
You hold an Animal*, but the Dog underneath is the one that answers.
class Animal {
public:
virtual void makeSound() const; // the call goes through the wearer
};
const Animal* j = new Dog();
const Animal* i = new Cat();
j->makeSound(); // Dog speaks
i->makeSound(); // Cat speaksThe keyword virtual is the enchantment.
It tells the compiler to look at what the object really is at runtime, not at the type of the pointer holding it.
| Static type | Dynamic type | |
|---|---|---|
| What it is | The type written in the code (Animal*) |
The real object behind it (Dog) |
| Decided at | Compile time | Run time |
Used by virtual |
No | Yes |
| Used by normal calls | Yes | No |
Remove the enchantment and the Helmet speaks for itself.
That is exactly what WrongAnimal demonstrates:
class WrongAnimal {
public:
void makeSound() const; // NOT virtual
};
const WrongAnimal* w = new WrongCat();
w->makeSound(); // WrongAnimal speaks — the wearer is ignored
WrongCat wc;
wc.makeSound(); // only as a concrete WrongCat does the cat speak[!NOTE] The return type of an overriding function does not need to repeat
virtual, but writing it on every override documents intent.
Once a function isvirtualin the base, it isvirtualin every derived class whether you type the word or not.
A wearer is more than the Helmet. There is a mind underneath.
When you put the Helmet away, you must also let that mind go.
class Animal {
public:
virtual ~Animal(); // mandatory the moment deletion happens through a base pointer
};
const Animal* j = new Dog();
delete j; // Dog::~Dog() runs first, THEN Animal::~Animal()Without the virtual, delete j only calls ~Animal().
Dog’s destructor never runs, the Brain it owns is never freed, and you leak a soul on every delete.
Dog destructor called ← derived first
Animal destructor called ← base last
[!WARNING] Non-virtual destruction through a base pointer is undefined behavior, not just a leak.
If a class has a singlevirtualfunction, give it avirtualdestructor. There is no reason not to.
The Helmet of Nabu cannot walk the earth on its own. It must be worn.
In ex02, Animal becomes something you can never instantiate directly.
class Animal {
public:
virtual void makeSound() const = 0; // pure virtual — the "= 0" is mandatory
};The = 0 makes makeSound() a pure virtual function.
A class with even one pure virtual function is abstract: it has no complete form of its own.
Animal test; // compile error: cannot declare variable 'test' to be of abstract type 'Animal'| Concrete class | Abstract class | |
|---|---|---|
| Has a pure virtual? | No | Yes (at least one) |
| Can you instantiate it? | Yes | No |
| Can you hold a pointer to it? | Yes | Yes |
| Role | A wearer (Dog, Cat) |
The Helmet itself |
Dog and Cat stay exactly as they were in ex01. They override makeSound(), so they are complete and they can be worn.
[!NOTE] An abstract class still has constructors and a destructor. They run when a derived object is built and destroyed.
You just cannot create the base alone.
Every host carries their own mind. Copy the wearer and you must copy the mind too, not share it.
In ex01, Dog and Cat each own a Brain* allocated with new.
class Dog : public Animal {
private:
Brain* _brain; // owned, heap-allocated
};
// Copy constructor — a NEW brain, deep-copied
Dog::Dog(const Dog& other) : Animal(other), _brain(new Brain(*other._brain)) {}
// Copy assignment — free the old brain, clone the new one
Dog& Dog::operator=(const Dog& other) {
if (this != &other) {
Animal::operator=(other);
delete _brain;
_brain = new Brain(*other._brain);
}
return *this;
}The evaluator’s test, and the reason OCF matters here:
Dog basic;
{
Dog tmp = basic; // deep copy → tmp gets its OWN brain
} // tmp dies, deletes ITS brain — basic is untouched
basic.getBrain(); // still valid[!WARNING] A shallow copy makes
tmpandbasicshare oneBrain.
Whentmpleaves the scope it deletes that shared brain, andbasicis left pointing at freed memory → double free on its own destruction.
The Lords of Order do not hand you an implementation. They hand you a pact: these powers must exist.
An interface is a class that is only pure virtual functions plus a virtual destructor. No data, no bodies.
class ICharacter {
public:
virtual ~ICharacter() {}
virtual std::string const& getName() const = 0;
virtual void equip(AMateria* m) = 0;
virtual void unequip(int idx) = 0;
virtual void use(int idx, ICharacter& target) = 0;
};
class IMateriaSource {
public:
virtual ~IMateriaSource() {}
virtual void learnMateria(AMateria*) = 0;
virtual AMateria* createMateria(std::string const& type) = 0;
};Character implements ICharacter. MateriaSource implements IMateriaSource.
Anyone holding an ICharacter* knows the pact will be honored, without knowing or caring which concrete class fulfills it.
[!NOTE] Interfaces are the only classes in CPP04 exempt from full Orthodox Canonical Form.
The eval checks OCF on every other class —AMateria,Ice,Cure,Character,MateriaSource.
A spell is not moved from hand to hand. It is copied, and the copy is what you carry.
AMateria is abstract because its clone() is pure virtual: the base cannot copy itself, only a concrete spell knows how.
class AMateria {
public:
virtual AMateria* clone() const = 0; // pure → AMateria stays abstract
virtual void use(ICharacter& target);
protected:
std::string _type;
};
AMateria* Ice::clone() const { return new Ice(*this); } // a brand-new, independent Ice
AMateria* Cure::clone() const { return new Cure(*this); }createMateria("ice") returns a clone() of a learned template. equip() takes ownership of that pointer. Whoever owns it, frees it.
[!WARNING]
unequip()must not delete the materia. It drops it on the floor and the caller becomes responsible for it.
The matching trap:equip()/learnMateria()on a full container must not silently leak the pointer they were handed. Free the overflow, or hand ownership back to the caller, but never lose it.
Calling a non-virtual method through a base pointer
Without virtual, animal->makeSound() calls Animal::makeSound() no matter what the object really is. That is the whole point of WrongAnimal — it is the wrong way, on purpose.
Missing virtual ~Animal()
const Animal* a = new Dog();
delete a; // only ~Animal() runs → Dog's Brain leaks. UB, not just a leak.Shallow copy of a Brain owner
// WRONG — shared brain, double free
Dog::Dog(const Dog& o) : Animal(o), _brain(o._brain) {}
// RIGHT — independent brain
Dog::Dog(const Dog& o) : Animal(o), _brain(new Brain(*o._brain)) {}Trying to instantiate an abstract class
Animal test; must fail to compile once makeSound() is = 0. If it compiles, the class is not actually abstract.
Leaking on a full inventory or full source (ex03)
Equipping a 5th materia into a 4-slot Character, or teaching a 5th to a MateriaSource, must not leak the pointer. Either delete it on overflow, or make sure the caller deletes it.
Empty copy assignment in OCF
The eval rejects empty/cosmetic OCF. A copy assignment that prints a message but copies nothing is a fail — deep-copy the owned resources.
# ──── ex00 ────
# Verify: Animal* on a Dog/Cat calls the DERIVED makeSound()
# Verify: WrongAnimal* on a WrongCat calls the BASE (non-virtual) sound
cd ex00 && make
./ex00
valgrind --leak-check=full ./ex00
# ──── ex01 ────
# Verify: copying a Dog/Cat gives it its OWN Brain (different address)
# Verify: deleting an Animal* to a Dog frees the Brain (no leak)
cd ex01 && make
./ex01
valgrind --leak-check=full ./ex01
# ──── ex02 ────
# Verify: 'Animal test;' fails to compile (abstract type)
# Verify: Dog and Cat still behave exactly like ex01
cd ex02 && make
./ex02
valgrind --leak-check=full ./ex02
# ──── ex03 ────
# Verify: subject example prints the ice bolt then the heal
# Verify: createMateria("unknown") returns NULL
# Verify: Character copy/assignment deep-copy the inventory
cd ex03 && make
./ex03
valgrind --leak-check=full ./ex03Abstractness check for ex02:
Animal test; // must NOT compile: "cannot declare variable 'test' to be of abstract type 'Animal'"Deep-copy check for ex01:
Dog basic;
basic.getBrain()->ideas[0] = "Fetch the ball";
Dog tmp = basic;
// tmp.getBrain() != basic.getBrain() → independent brainsvirtual function specifierYou can hold the Helmet. You can put it on and look the part.
But it is the wearer underneath, the override, the deep copy, the freed soul, that decides whether the magic is real.
Whoever puts on the Helmet answers a call older than themselves. Make sure your destructor is virtual when they take it off.