padraic.xyz

Object Oriented Code

These notes are my own based on the full version, which can be accessed at the RITS website.

Keyword Glossary

Object Oriented Recap

Note: the virtual keyword, both on a base class and a derived class, signifies that the method is designed to be overwritten by the derived class. For example, consider classes

class Shape {
 public:
 Shape();
 virtual void rotate(const double& degrees)=0;
}

class Rectangle : public shape {
 public:
 Rectangle();
 virtual void rotate(const double& degrees);
}

It can be helpful to define a default implementation for the base class in some cases, but that's only if the base class is 'abstract' (unusable on its own without further details from a derived classs). However, setting it to 0 behaves specially for abstract classes; it tells C that the virtual method HAS to be implemented in derived classes. This is called a pure virtual function . A pure abstract class (also called an interface) has all methods as pure.

This ability is called subtype polymorphism : the ability of derived classes to reimplement methods defined in the base class.

_Note that there also exists _ parametric _ and _ ad-hoc _ polymorphism. _


The above example is public inheritance, namely that the public methods of the base class are also public in the derived class. This is NOT the case for private inheritance, where public methods become private to the derived class. :::C++ class Shape { public: Shape(); void setVisible(const bool& isVisible) { m_IsVisible = isVisible; } virtual void rotate(const double& degrees) = 0; virtual void scale(const double& factor) = 0; // + other methods private: bool m_IsVisible; unsigned char m_Colour[3]; // RGB double m_CentreOfMass[2]; };

class Rectangle : public Shape {
 public:
 Rectangle();
 virtual void rotate(const double& degrees);
 virtual void scale(const double& factor);
 // + other methods
 private:
 double m_Corner1[2];
 double m_Corner2[2];
};

class Circle : public Shape {
 public:
 Circle();
 virtual void rotate(const double& degrees);
 virtual void scale(const double& factor);
 // + other methods
 private:
 float radius;
};
int main(int argc, char** argv)
{
 Circle c1;
 Rectangle r1;
 Shape *s1 = &c1;
 Shape *s2 = &r1;

 // Calls method in Shape (as not virtual)
 bool isVisible = true;
 s1->setVisible(isVisible);
 s2->setVisible(isVisible);

 // Calls method in derived (as declared virtual)
 s1->rotate(10);
 s2->rotate(10);
}

Inheritence and Design

It is easy for overuse inheritence and to arrive at a point where inheritence is actually impeding your code. The common phrase used is to favour 'compositon' over inheritence. Consider the 'square/rectangle' example. :::C #include

class Rectangle {
public:
 Rectangle() : m_Width(0), m_Height(0) {};
 virtual ~Rectangle(){};
 int GetArea() const { return m_Width*m_Height; }
 virtual void SetWidth(int w) { m_Width=w; }
 virtual void SetHeight(int h) { m_Height=h; }
protected:
 int m_Width;
 int m_Height;
};

class Square : public Rectangle {
public:
 Square(){};
 ~Square(){};
 virtual void SetWidth(int w) { m_Width=w; m_Height=w; }
 virtual void SetHeight(int h) { m_Width=h; m_Height=h; }
};

int main() 
{
 Rectangle *r = new Square();
 r->SetWidth(5);
 r->SetHeight(6);
 std::cout << "Area = " << r->GetArea() << std::endl;
}

We have over-ridden SetWidth and SetHeight, and thus what we are left with is a square with area 36. Our SetWidth and Set Height calls have had side-effects built in, which means this example breaks our expectation. Valid behaviour for a rectangleis NOT valid for a square.

The principle of valid inheritence is properly called Liskov's Substitution principle : If S is a subtype of T, then objects of type T may be replaced by those of type S without altering any of the desirable properties of that programme.

How can we do that meaningfully? Our class can be considered as a collection of data and operations on that data, and thus we have to look at:

  1. Preconditions: These cannot be strengthed by subtyping
  2. Postconditions: These cannot be weaked by subtyping
  3. Invariants: These must be preserved

In general, it's a good rule of thumb that your inheritence is probably broken if:

Composition vs. Inheritence

There are two types of 'relationship' in OOP: 'is-a' relationships, which underlie inheritence, and 'has-a' relationship e.g. a Car object has a pointer to an associated engine instance. This has-a principle can be further differentiated

Either of these relations can also be called associations, a formal term for has-a relationships between classes.

Dependency Injection

Consider the following example :::C #include

class Bar {
};

class Foo {
public:
 Foo()
 {
 m_Bar = new Bar();
 }

private:
 Bar* m_Bar;
};

int main()
{
 Foo a;
}

Here we've used composition, and it seems like we've done the correct thing? But we have hard-coded the type dependency, and duplicated instantiation code. Instead, we should 'inject' the dependecy either through the constructor or through a setter pattern.

Which you perfer depends on personal preference to an extent but constructor pattern avoids the case where we instantiate a class without it's dependency.

Construction Patterns

There are a number of design 'patterns' that have emerged over the years. The most famous reference for design patterns is the so called 'Gang of Four' book , which sets out a number of known stratergies like

A strong example is the strategy pattern, which emerges frequently in scientific programming was we tpyically focus on algorithm implementation. A UML diagram illustrating the patten can be seen on wikipedia . In particular, our programme could have a calculator class, with an abstract virtual method GetAverage. This method then has derived classes MeanCalculator, MedianCalculator etc, which each implement GetAverage. Thus, the Calculator is a pure abstract base class which lets us seemlessly test each implementation. Our code would implement a pointer to a calculator instance, which we instantiate with a derived strategy.

Smart Pointers

Our aim is to write code that can be reused, reinserted and substituted as needed. But raw pointers cause serious problems, especially in a production environment. Pointers can be NULL, pointers can be 'dangling' (memory leaks), we don't know that the value is meaningful, do we delete it when we're done, do we delete the pointer or free it or call a destructor method, is it a single object or an array?

Instead, we should use SmartPointers. These classes 'quack like a pointer', but automatically handle the creation and deletion of objects, and define the ownership models in our OOP code. These notes are based on a paper by Kieras , and Effective C++.

Smart pointers are implemented by different software frameworks, but we will focus on the standard library model. There are three types:

  1. std::unique_ptr : models strong has-a relationships with unique ownerships
  2. std::shared_ptr : models weak has-a relationships with shared ownerships.
  3. std::weak_ptr : a temporary reference which breaks circular referencing

Recall that 'stack allocated' variables are automatically deleted when the stack they are a part of fully unwinds. However, if we initialise an object and store its pointer, the stack allocated pointer is deleted, and the fraction remains on the heap. We can force the Fraction to be deleted by using a unique_ptr instead, which deleted the heap allocated variable when the pointer itself is deleted.

Unique_ptr 's are instantiated with a template-like syntax. Their API avoids ever accessing the raw pointer by providing getters and setters, and in turn prevent copying or assigning to them. :::C #include "Fraction.h" #include #include

int main() {
 std::unique_ptr<Fraction> f(new Fraction(1,4));
 // std::unique_ptr<Fraction> f2(f); // compile error

 std::cerr << "f=" << f.get() << std::endl;

 std::unique_ptr<Fraction> f2;
 // f2 = f; // compile error
 // f2.reset(f.get()); // bad idea

 f2.reset(f.release());
 std::cout << "f=" << f.get() << ", f2=" << f2.get() << std::endl;

 f = std::move(f2);
 std::cout << "f=" << f.get() << ", f2=" << f2.get() << std::endl;
}

Sidenote: we can block our class from having copy constructors or assignment operators by leaving the method private and unimplemented.

Shared Pointers

TODO: Make notes here

Weak Pointers

Ooops turns out Notion isn't that good for note-taking...

RAII Pattern

This stands for Resource Allocation Is Initialisation pattern: we insist that all resources are obtained in the costructor, and all are released in the destructor. This prevents an object being created which is unusable, and by cominbing this with smart pointers to associated obejcts guarantees that objects o the stack are guaranteed to be destroyed when a stack is unwound (e.g on exception).

Related to RAII is the notion of a mutex, which is a shorthand for mutual exclusion. This is a property of concurrent programmes, where the function guarantees that it and only it is currently modifying a resource that is shared between programmes. This is important when working with multithreaded programmes.

Programming to Interfaces

Programming to interfaces is important because the typical research approach of 'just starting to hack' ends up mixing interface, how the code is accessed, vs implementation. This can be a result for example of a client of a class having implict dependency on the implementation. If we instead design our programme by starting with the interface, we break the system down into iterable, testable chunks. These chunks will be more likely to be self-contained and fucntion correctly, and we keep different subroutines from 'infecting' the rest of the code.

In summary, then: