CSC 347 - Concepts of Programming Languages

Inheritance and Dynamic Dispatch

Instructor: Stefan Mitsch

Learning Objectives

How do we support code sharing between classes?

What should happen when methods have the same name?

  • Identify classes and inheritance
  • Describe the difference between static and dynamic dispatch

Scala: Which toString?

  • What is the type of x? Which toString is invoked?
  1. class Animal { override def toString () = "Animal" }
  2. class Bird extends Animal { override def toString () = "Bird" }
  3. class Cat extends Animal { override def toString () = "Cat" }
  4. val xs = Array[Animal] (new Bird(), new Cat ())
  5. for (x <- xs) println (x.toString())
  • x has
    • static type Animal
    • dynamic type Bird/Cat
  • Dynamic dispatch, late binding: use dynamic type (of object)

  • Output at runtime
    1. Bird
    2. Cat

Java: Which toString?

  • What is the type of x? Which toString is invoked?
  1. class Animal { public String toString () { return "Animal"; } }
  2. class Bird extends Animal { public String toString () { return "Bird"; } }
  3. class Cat extends Animal { public String toString () { return "Cat"; } }
  4. Animal[] xs = { new Bird(), new Cat () };
  5. for (Animal x : xs) System.out.println (x.toString());
  • x has
    • static type Animal
    • dynamic type Bird/Cat
  • Dynamic dispatch, late binding: use dynamic type (of object)
  • Output at runtime
    1. Bird
    2. Cat

C++: Which toString?

  • What is the type of x? Which toString is invoked?
  1. class Animal { public: string to_string() { return "Animal"; } };
  2. class Bird: public Animal { public: string to_string() { return "Bird"; } };
  3. class Cat: public Animal { public: string to_string() { return "Cat"; } };
  4. Animal *xs[] = { new Bird(), new Cat() };
  5. for (Animal *x : xs) cout << x->to_string() << endl;
  • x has
    • static type Animal
    • dynamic type Bird/Cat
  • Static dispatch, early binding: use static type (of variable)
  • Output at runtime
    1. Animal
    2. Animal

C++: Which toString?

  • What is the type of x? Which toString is invoked?
  1. class Animal { public: virtual string to_string() { return "Animal";
  2. class Bird: public Animal { public: virtual string to_string() { return "Bird"; }
  3. class Cat: public Animal { public: virtual string to_string() { return "Cat"; }
  4. Animal *xs[] = { new Bird(), new Cat() };
  5. for (Animal *x : xs) cout << x->to_string() << endl;
  • x has
    • static type Animal
    • dynamic type Bird/Cat
  • Output at runtime
    1. Bird
    2. Cat
  • C++ dynamically dispatches virtual methods only when object accessed with a pointer/reference

C++: Which toString?

  • What is the type of x? Which toString is invoked?
  1. class Animal { public: virtual string to_string() { return "Animal";
  2. class Bird: public Animal { public: virtual string to_string() { return "Bird"; }
  3. class Cat: public Animal { public: virtual string to_string() { return "Cat"; }
  4. Animal xs[] = { Bird(), Cat() };
  5. for (Animal x : xs) cout << x.to_string() << endl;
  • x has
    • static type Animal
    • dynamic type Animal
  • Output at runtime
    1. Animal
    2. Animal
  • Truncated object, stored in place: C++ truncates the object, coercing to parent class

With Lambda Notation

  • What is the type of fs[i]? Which apply method?
  1. interface Fn { int apply (int x); }
  1. Fn[] fs = new Fn[3];
  2. for (int i = 0; i < fs.length; i++) {
  3. int j = i + 1; // effectively final
  4. fs[i] = x -> x + j;
  5. }
  6. for (int i = 0; i < fs.length; i++) {
  7. System.out.println(i + "=" + fs[i].apply(20));
  8. }
  9. // prints 0=21, 1=22, 2=23

With Anonymous Classes

  1. interface Fn { int apply (int x); }

Lambda notation

  1. Fn[] fs = new Fn[2];
  2. for (int i = 0;
  3. i < fs.length;
  4. i++) {
  5. int j = i + 1;
  6. fs[i] = x -> x + j;
  7. }

Anonymous classes

  1. Fn[] fs = new Fn[2];
  2. for (int i = 0;
  3. i < fs.length;
  4. i++) {
  5. int j = i + 1;
  6. fs[i] = new Fn() {
  7. int apply (int x) {
  8. return j + x;
  9. }
  10. }
  11. }

Explicit classes

  1. Fn[] fs = new Fn[2];
  2. class F implements Fn {
  3. public int apply(int x) {
  4. return x + 1;
  5. }
  6. }
  7. class G implements Fn {
  8. public int apply(int x) {
  9. return x + 2;
  10. }
  11. }
  12. fs[0] = new F();
  13. fs[1] = new G();

Which Object?

  • Which object are we accessing when invoking g() and f(x-1) in line 4?
  • Class definition
    1. class A {
    2. int f (int x) {
    3. System.out.println("A.f(" + x + ")");
    4. return (x == 0) ? g() : f(x - 1);
    5. }
    6. int g () { System.out.println ("A.g()"); return 0; }
    7. }
  1. A x = new A();
  2. x.f(2);
  • Output at runtime
    1. A.f(2)
    2. A.f(1)
    3. A.f(0)
    4. A.g()
  • The object referenced by x; what is its explicit name inside class A?

Which Object: this

  • What are the static and dynamic types of this?
  • Class definition
    1. class A {
    2. int f (int x) {
    3. System.out.println("A.f(" + x + ")");
    4. return (x == 0) ? this.g () : this.f (x - 1);
    5. }
    6. int g () { System.out.println ("A.g()"); return 0; }
    7. }
  1. A x = new A();
  2. x.f(2);
  • Output at runtime
    1. A.f(2)
    2. A.f(1)
    3. A.f(0)
    4. A.g()
  • Reference this has
    • static type of enclosing class (here: A)
    • dynamic type of object (here: A)

Static and Dynamic Type of this

  • What are the static and dynamic types of x and this?
  • Class B inherits f, overrides g
    1. class A {
    2. int f (int x) {
    3. System.out.println("A.f(" + x + ")");
    4. return (x == 0) ? this.g() : this.f(x - 1);
    5. }
    6. int g () { System.out.println ("A.g()"); return 0; }
    7. }
    8. class B extends A {
    9. int g () { System.out.println ("B.g()"); return 0; }
    10. }
  1. B x = new B();
  2. x.f(2);
  • Output at runtime
    1. A.f(2)
    2. A.f(1)
    3. A.f(0)
    4. B.g()
  • x has static type B, dynamic type B
  • this has static type A, dynamic type B

Static and Dynamic Type of this

  • What are the static and dynamic types of x and this?
  1. class A {
  2. int f (int x) {
  3. System.out.println("A.f(" + x + ")");
  4. return (x == 0) ? this.g() : this.f(x - 1);
  5. }
  6. int g () { System.out.println("A.g()"); return 0; }
  7. }
  8. class B extends A {
  9. int f (int x) { System.out.println("B.f(" + x + ")"); return this.f(x); }
  10. int g () { System.out.println("B.g()"); return 0; }
  11. }
  1. A x = new B();
  2. x.f(1);

What happens?

  • Output at runtime
    1. B.f(1)
    2. B.f(1)
    3. ... // infinite loop
  • Why?
  • x: static type A, dynamic type B
  • this (l. 4): static A, dynamic B
  • this (l. 9): static B, dynamic B

How to Access Method in Super-Class?

  • Access A.f with super
  1. class A {
  2. int f (int x) {
  3. System.out.println("A.f(" + x + ")");
  4. return (x == 0) ? this.g() : this.f(x - 1);
  5. }
  6. int g () { System.out.println("A.g()"); return 0; }
  7. }
  8. class B extends A {
  9. int f (int x) { System.out.println("B.f(" + x + ")"); return super.f(x); }
  10. int g () { System.out.println ("B.g()"); return 0; }
  11. }
  1. A x = new B ();
  2. x.f(1);

What happens?

  • Output at runtime
    1. B.f(1)
    2. A.f(1)
    3. B.f(0)
    4. A.f(0)
    5. B.g()

Document and Check Inheritance

Let programmers specify how classes can be instantiated?
Let programmers decide which methods can/must/cannot be overridden?

Abstract Classes

  • Abstract class A
  1. abstract class A {
  2. int f (int x) { /* ... */ }
  3. int g () { /* ... */ }
  4. }
  1. A x = new A();

What happens?

  • Compile error: abstract classes cannot be instantiated
    1. | Error:
    2. | A is abstract; cannot be instantiated
    3. | A x = new A ();
    4. | ^------^

Abstract Methods

  • Abstract method g
  1. abstract class A {
  2. int f (int x) { /* ... */ }
  3. abstract int g ();
  4. }
  1. class B extends A {
  2. }

What happens?

  • Compile error: Abstract methods must be overridden in concrete subclass
    1. | Error:
    2. | B is not abstract and does not override abstract method g() in A
    3. | class B extends A {
    4. | ^------------------...

Final Methods

  • Final method f
  1. abstract class A {
  2. final int f(int x) { /* ... */ }
  3. abstract int g();
  4. }
  1. class B extends A {
  2. int f(int x) { /* ... */ }
  3. int g() { /* ... */ }
  4. }

What happens?

  • Compile error: final methods cannot be overridden
    1. | Error:
    2. | f(int) in B cannot override f(int) in A
    3. | overridden method is final
    4. | int f (int x) { ... }
    5. | ^-------------------^

Use Case: Design Pattern Template Method

Defines the skeleton of an algorithm, deferring varying steps to subclasses

  • Factor common behavior into a base class to avoid code duplication
  • Implement algorithm structure once, varying behavior in subclasses

  • In functional programming?
  1. abstract class AbstractClass {
  2. public final void templateMethod() {
  3. step1();
  4. operation1();
  5. step2();
  6. }
  7. protected abstract void operation1();
  8. private void step1() { /* ... */ }
  9. private void step2() { /* ... */ }
  10. }
  11. class ConcreteClass extends AbstractClass {
  12. @override
  13. protected void operation1() { /* ... */ }
  14. }

Summary

  • Inheritance: links classes statically at compile time

Static Dispatch

  • Early binding at compile time
  • Select method based on type of reference

Dynamic Dispatch

  • Late binding at runtime
  • Select method based on type of object

Examples: C++

  1. class Animal { public: virtual string to_string() { return "Animal"; } };
  2. class Bird: public Animal { public: virtual string to_string() { return "Bird"; } };
  3. class Cat: public Animal { public: virtual string to_string() { return "Cat"; } };
  • Pointers + virtual:
    1. Animal* a = new Bird ();
    2. cout << a->to_string() << endl;
    3. a = new Cat ();
    4. cout << a->to_string() << endl;
  • Reference + virtual:
    1. void print(Animal& a) {
    2. cout << a.to_string() << endl;
    3. }
    4. Bird b = Bird ();
    5. Cat c = Cat ();
    6. print(b);
    7. print(c);
  • Static types:
    1. Bird* b = new Bird ();
    2. cout << b->to_string() << endl;
    3. b = new Cat ();
    4. cout << b->to_string() << endl;

Examples: Scala

  1. class A:
  2. def f () = { println("A.f"); this.g () }
  3. def g () = { println("A.g"); this.h () }
  4. def h () = println("A.h")
  5. class B extends A:
  6. override def f () = { println("B.f"); super.f () }
  7. override def h () = println("B.h")
  8. def i () = println("B.i")
  • Static A, dynamic A
    1. val a:A = new A()
    2. a.f()
    3. a.g()
    4. a.h()
    5. a.i()
  • Static A, dynamic B
    1. val a:A = new B()
    2. a.f()
    3. a.g()
    4. a.h()
    5. a.i()
  • Static B, dynamic B
    1. val b:B = new B()
    2. b.f()
    3. b.g()
    4. b.h()
    5. b.i()

# <span class="fa-stack"><i class="fa-solid fa-circle fa-stack-2x"></i><i class="fa-solid fa-book fa-stack-1x fa-inverse"></i></span> Delegation-based Languages - Create objects rather than instantiate classes - Use delegation instead of inheritance - Example: Javascript <div class="grid grid-cols-3 gap-4"> <div class="col-span-2"> ```js var A = { f (x) { console.log ("A.f (" + x + ")") return (x == 0) ? this.g () : this.f (x - 1); }, g () { console.log ("A.g ()"); return 0; } } var B = { f (x) { console.log ("B.f (" + x + ")"); return super.f (x); }, g () { console.log ("B.g ()"); return 0; } } ``` </div> <div> - Link objects ```js Object.setPrototypeOf(B, A); B.f (2); ``` * Output at runtime ```sh B.f (2) A.f (0) A.f (2) B.g () B.f (1) $1 ==> 0 A.f (1) B.f (0) ``` </div> </div> ---