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 (even "primitive" types).
Object-oriented programming requires some special things to work.
public class Book {
public Book();
public void report();
public void setTitle(string t);
// ...
private string title;
private string author;
private string isbn;
private int quantity;
private int prices;
}
Classes are implemented in similarly to records where the variables are stored together in memory:
Member functions are implemented simply as functions that take the object as the first parameter. In Java this parameter is a reference and in C++ it is a pointer.
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) {
}
In Python, the reference to the first parameter is called "self" and must be passed explicitly, but the principle is the same.
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;
};
In Python, constructor calls are always explicit. In Java, they usually are, except in the case of the "wrapper" classes such as Integer, Double, etc. This type of conversion is called "boxing", and the reverse is called "unboxing".
Destructors are functions that exist in C++, and 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. Languages with garbage collectors do not need destructors, since the principle reason for having them is to release any memory that the object has allocated..
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.
When we call a function or method, we must bind the call to the actual function. This can be done "early", which means at compile time. In languages like C, all bindings are done early. With polymorphism, however, we can't get away with early binding of method calls.
For example, in code like this:
Enemy e = EnemyGenerator.generate()
e.attack(player);
It's possible that the object referenced by e
is not actually of
type Enemy
, but rather a subclass. So we cannot bind the call of
attack
to a method at compile time. We must wait until runtime to
know what implementation of attack
to call. This is called late
binding or dynamic dispatch.
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.
Thus, we have to do more work every time we call a method, which is the price of polymorphism. In Java and Python
dynamic dispatch is always used. In C++, the programmer must specify if a method should be called using
early or late binding. The default is early binding, and the virtual
keyword marks a function
as requiring late binding.
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 itself as "virtual" which tells the compiler to insert two vtables for the class D. Java, C# and many others do not allow multiple inheritance for this reason.
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.
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.
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 draw function, either from Object or Ship
asteroid.draw(); // will match the best draw function, either from Object or Asteroid
Here, the object in the method call is treated specially. At runtime, the program will look at the current type using the vtable and find the correct method 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:
class Entity {
public void collide(Entity other) {
System.out.println("Generic version!");
}
}
class Ship extends Entity {
public void collide(Ship other) {
System.out.println("Ship collides with Ship.");
}
public void collide(Asteroid other) {
System.out.println("Ship collides with Asteroid.");
}
}
class Asteroid extends Entity {
public void collide(Ship other) {
System.out.println("Asteroid collides with Ship.");
}
public void collide(Asteroid other) {
System.out.println("Asteroid collides with Asteroid.");
}
}
public class Main {
public static void main(String[] args) {
Entity d = new Ship();
Entity c = new Asteroid();
c.collide(d);
}
}
However this only ever results in the generic Entity collide function being called, regardless of what kind of Entities are involved. This is because Java (and Python or C++) 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 entity ()
())
(defclass asteroid (entity)
())
(defclass ship (entity)
())
; define each method
(defmethod collide ((a entity) (b entity))
(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 entity
(collide (make-instance 'asteroid)
(make-instance 'ship))
Copyright © 2024 Ian Finlayson | Licensed under a Creative Commons BY-NC-SA 4.0 License.