Home CPSC 401

Object Oriented Programming

Overview

Object-oriented programming is perhaps the most popular style of programming. Most modern languages provide object-oriented features.

Some languages, such as Java and C# enforce that all code must belong to a class.

Others, such as Ruby and Smalltalk (a language that pioneered OOP), have everything in the language represented with objects.

Object-oriented programming requires some special things to work.


Implementation of Classes & Objects


class Book {
    private:
        string title;
        string author;
        string isbn;

        int quantity;
        int prices;

    public:
        Book();
        void report();
        void setTitle(string t);
        // ...
};

Classes are implemented in similarly to records where the variables are stored together in memory:

Member functions are implemented as functions that take the object as the first parameter:


struct Book {
    string title;
    string author;
    string isbn;

    int quantity;
    int prices;
};

void construct(Book* this) {

}

void report(Book* this) {

}

void setTitle(Book* this, string t) {

}

Some languages use "self" instead of "this" and many use a reference to the current object instead of a pointer.


Constructors & Destructors

A constructor is a function that is called when an object is created. Usually constructors are called explicitly by the programmer:


// Java:
Thing t1 = new Thing();

# python:
t1 = Thing()

In C++, constructors can be called implicitly in some situations:


class Thing {
  public:
    Thing(int value) {
      v = value;
    }

    int v;
};

Thing t1 = 12;

Or, even harder to see:


void function(Thing t) {


}

int main() {
  function(5);
  return 0;
}

To force constructor calls to be explicit, it can be marked as explicit:


class Thing {
  public:
    explicit Thing(int value) {
      v = value;
    }

    int v;
}; 

Destructors are functions that are called when an object is destroyed. These are automatically called by the compiler when an object's lifetime ends.


Thing t1;

int main() {
  Thing t2;

  while(a < b) {
    Thing t3;
  
    // ...
  }

  Thing* t4 = new Thing;

  // ...

  delete t4;

  return 0;
}

When are the destructors for t1, t2, t3 and t4 called?

The compiler must insert calls to the destructor itself. Most languages with garbage collectors do not have destructors.


Inheritance

Inheritance is an important feature of OOP. It allows for code reuse and polymorphism.

When a class signifies that it inherits another, all of the data values from the base class will be included in the derived class. Also, all base member functions will be callable for the derived class.

Member functions can also be overridden. Some languages allow for classes or functions to be marked "final" or "sealed" which means they can not be derived from / overridden.


Dynamic Dispatch

The functions we have discussed so far have used static binding of function calls to function bodies. However, with polymorphism, we can also have dynamic, or late binding to functions:


class Base {
  public void print() { 
    System.out.println("Base!");
  }
}

class Derived extends Base {
  public void print() { 
    System.out.println("Derived!");
  }
}

public class Example {
  public static void main(String[] args) {
    Base a, b;

    a = new Base();
    b = new Derived();

    a.print();
    b.print();
  }
}

Here the calls to print must use late binding. When generating code, the compiler cannot know for certain which function to match a call to "print" with.

This is handled with a virtual method table or vtable. This table matches function names to where they are defined. There is one vtable created for each class.

Then each object that is created stores a pointer to the virtual table of the class it currently stores:

Every time a function is called, the function's location is first looked up inside of the vtable.


Multiple Inheritance

Some languages allow multiple inheritance where a class inherits from multiple base classes. This can open us up to a few issues.

One is that we can have multiple variables or functions with the same name:


class A { 
  public:
    A() { 
      x = 1;
    }   
    int x;
};

class B { 
  public:
    B() { 
      x = 2;
    }   
    int x;
};

class C : public A, public B { 

};

This is handled by the compiler requiring us to specify which one is meant:


int main() { 
  C c;
  cout << c.A::x << endl;
  return 0;
}

A worse problem is the "diamond problem" or the "deadly diamond of death" problem which is illustrated below:

Here D inherits from B and C where each of those have inherited from A. The issue with this is whether D will have one or two copies of A's data.

The way inheritance typically works will give us two copies!


class A { 
  public:
    void f() {cout << "A";}
};

class B : public A { 
};

class C : public A { 
};

class D : public B, public C { 
};                                                                                                                   


int main() { 
  D d;
  d.f();
  return 0;
}

x.cpp: In function 'int main()':
x.cpp:21:5: error: request for member 'f' is ambiguous
x.cpp:6:10: error: candidates are: void A::f()
x.cpp:6:10: error:                 void A::f()

The compiler doesn't know if we want A::f or A::f.

In C++, this can be solved by marking the inheritance as "virtual". Java, C# and many others do not allow multiple inheritance for this reason.


Open Classes

Most languages have closed classed. Once a class is defined, new methods cannot be added into it.

One exception is Ruby which allows classes to be extended arbitrarily after they are defined:


class Fixnum
  def factorial()
    x = 1
    for i in 1..self
      x = x * i
    end
    return x
  end 
end

# now we can call our new method
puts (7.factorial())

This allows the extension of classes without needing to inherit from them. It can also make programs difficult to understand if abused.


Prototype-Based OOP

The most common way languages provide OOP is through classes. There is another way, however, used by languages such as Javascript and Lua.

That way is to build a prototype of an object, and then clone new objects from it.

In Javascript, this is done by writing a function that returns a new object:


// create an object
function Book(isbn, title) {
  this.isbn = isbn;
  this.title = title;
}

// add a member function
Book.prototype.print = function()
{
  alert(this.isbn + ':' this.title);
}

Objects are created by calling the constructor function:


var b1 = new Book('123456789', 'Some Title');
b1.print();

Objects can also be created with object literals:


var b2 = {isbn: '987654321', title: 'Other Title'};

Inheritance is not done by making a new class and specifying a base class. It is done by creating an object of the base class, then adding new variables or functions into it:


// make a new object by extending an existing one
function BookForSale(isbn, title, price, quantity) {
  Book.call(this, isbn, title);
  this.price = price;
  this.quantity = quantity;
}

// set the constructor to the new class
BookForSale.prototype.constructor = BookForSale;

// override a function
BookForSale.prototype.print() = function {
  alert(this.isbn + ':' this.title + ' costs ' + this.price);
}

The BookForSale function clones the object returned from Book and adds new things into it. There is no place where classes are formally defined.

What are the trade-offs between class-based and prototype-based OOP?


Multiple Dispatch

Most OOP languages use "single-dispatch" which means that only one object in a function call has polymorphism applied to it.


// asteroid and ship are both Objects
Object ship = new Ship();
Object asteroid = new Asteroid();

ship.draw();       // will match the best speak function, either from Object or Ship
asteroid.draw();   // will match the best speak function, either from Object or Asteroid

Here, the object in the function call is treated specially. At runtime, the runtime will look at the current type and find the best function to call.

There can only be one object before the "." so only one object can be polymorphic at a time.

If we wanted a function to work on two objects, and behave differently, for each combination, then we could try this:


#include <iostream>

using namespace std;

class Object {
  public:
    virtual void collide(Object& other) {
      cout << "Generic version!\n";
    }   
};
class Asteroid;
class Ship : public Object {
  public:
    virtual void collide(Ship& other) {
      cout << "Ship collides with Ship.\n";
    }   
    virtual void collide(Asteroid& other) {
      cout << "Ship collides with Asteroid.\n";
    }   
};

class Asteroid : public Object {
  public:
    virtual void collide(Ship& other) {
      cout << "Asteroid collides with Ship.\n";
    }   
    virtual void collide(Asteroid& other) {
      cout << "Asteroid collides with Asteroid.\n";
    }   
};

int main() { 
  Object* d = new Ship();
  Object* c = new Asteroid();

  c->collide(*d);

  return 0;
}

However this only ever results in the Object::collide function being called, regardless of what kind of animals are involved. This is because C++ (and Java) do not support multiple dispatch.

With multiple dispatch, every object in a function call can be polymorphic, so we can write code like the above and have it work. Note that this is different than function overloading. In function overloading, the compiler only uses static binding based on the compile-time type. Polymorphism relies on the type at run-time.

Languages with multiple dispatch include Lisp variants, R, and Groovy. The following Common Lisp code uses multiple dispatch for the idea above:


; declare the three classes
(defclass object ()
  ())

(defclass asteroid (object)
 ())

(defclass ship (object)
 ())

; define each method
(defmethod collide ((a object) (b object))
  (format t "Generic One"))

(defmethod collide ((a ship) (b ship))
  (format t "Ship collides with Ship"))

(defmethod collide ((a ship) (b asteroid))
  (format t "Ship collides with Asteroid"))

(defmethod collide ((a asteroid) (b ship))
  (format t "Asteroid collides with Ship"))

(defmethod collide ((a asteroid) (b asteroid))
  (format t "Asteroid collides with Asteroid"))

; try calling a method on two objects
(collide (make-instance 'asteroid)
        (make-instance 'ship))

Copyright © 2018 Ian Finlayson | Licensed under a Creative Commons Attribution 4.0 International License.