Polymorphie in C++

Martin Sulzmann

Polymorphie

Polymorphie (griechisch Vielgestaltigkeit) ist ein Programmierkonzept. Zweck: Einheitliche Schnittstelle welche auf verschiedene Typen anwendbar ist. Es gibt verschiedene Formen von Polymorhpie, z.B.

Coercive Subtyping

Allgemeines Subtyping Prinzip:

Hier Coercive Subtyping: char \leq int \leq float \leq double

Beispiel

int func(float x) {
    if (x < 1.0) return 1;
    return 0;
}

void testCoerce() {
 int arg = 1;
 float res;

 // Typkorrekt weil int <= float,
 // d.h. jeder Wert vom Typ int kann auch an der
 // Programmstelle verwendet werden, an welcher float erwartet wird.
 res = func(arg);
}


// Compiler fuegt explizite Coercions ein!
// Hier wird das ganze simuliert durch die Funktion coerce.


float coerce(int x) {
  return (float)x;
}

void testCoerceTranslated() {
 int arg = 1;
 float res;

 res = coerce(func(coerce(arg)));
}


int main() {
  testCoerce();

  testCoerceTranslated();

  return 1;
}

Nominales Subtyping

Beispiel

// B <= A
// abgeleitet aus Klassendeklaration, in der Literatur als "nominal subtyping" bekannt.
class A {};
class B : public A {};


void h(A x) {}

void g(B x) {}

void testAB() {
  A a;
  B b;

  h(a);     // OK
  h(b);     // OK, weil B <= A
  // g(a);     // NICHT OK
  g(b);     // OK

}

int main() {

  testAB();

  return 1;
}

Subtyping und Varianz

Betrachte Typkonstruktoren, z.B. Pointer Typen.

Welche Beziehung gilt zwischen int* und float*?

Was ist mit B* und A*?

Pointer Subtyping is kovariant!

Falls tst \leq s dann t*s*t* \leq s*.

Wieso kovariant? Damit man “generischen” Code schreiben kann, siehe Generische Datentypen und Funktionen in C

Beispiel

// B <= A
// abgeleitet aus Klassendeklaration, in der Literatur als "nominal subtyping" bekannt.
class A {};
class B : public A {};


void h(A x) {}

void g(B x) {}

void testAB() {
  A a;
  B b;

  h(a);     // OK
  h(b);     // OK, weil B <= A
  // g(a);     // NICHT OK
  g(b);     // OK

}

// Abgeleitete Subtypbeziehung:
// B <= A   impliziert B* <= A*
void h2(A* x) {}

void g2(B* x) {}

void testAB_2() {
  A* a = new A();
  B* b = new B();

  h2(a);     // OK
  h2(b);     // OK, weil B <= A und daher auch B* <= A*
  // g2(a);     // NICHT OK
  g2(b);     // OK

  delete a;
  delete b;
}

// Das gleiche gilt auch fuer Arrays.
void h3(A x[]) {}

void g3(B y[]) {}

void testAB_3() {
  A a[] = { A(), A(), B() };  // B <= A

  B b[] = { B() };

  h3(b);  // OK, weil aus B <= A folgt B[] <= A[]

  g3(b);
  // g3(a);  // Nicht OK
}


int main() {

  testAB();
  testAB_2();
  testAB_3();

  return 1;
}

Vergleich zu Java

Array Subtyping in Java ist auch kovariant.

Wieso? Weil man damit “generische” Bibliotheken in Java 4 (for Java Generics) schreiben konnte.

Aber, das folgende Programm liefert einen Laufzeitfehler.

void main() {
String[] s;
s = new String[10];
test(s);
}

void test (Object [] a) {
  a[1] = new Point(10,20); // Crash!
}

Overloading

Gleicher Funktions-/Operator-Name aber verschiedene Verwendung (Anzahl Argumente und deren Typen).

C++ Regel: Instanz muss eindeutig bestimmbar sein via Argumenttypen.

Beispiel

#include <iostream>
#include <string>
using namespace std;

int funny(int x, int y) {
  return x;
}

char funny(char x, char y) {
  return y;
}

void testFunny() {

  cout << funny(1,2);
  cout << funny('a', 'b');

  // cout << funny(1, 'a');  // Ambiguous, nicht klar welche Instanz wir wollen

}

/*

// Folgendes ist nicht erlaubt, weil
// "functions that differ only in their return type cannot be overloaded"

int funny2() {
  return 1;
}

bool funny2() {
  return true;
}

void testFunny2() {

  bool x = funny2();

  int y = funny2();

}


*/


int main() {
  testFunny();
}

C++ Templates und Vergleich zu Java generics

C++ verwendet “Monomorphisation”: Für jede Instanz dupliziere den Programmcode.

Java verwendet ein “generisches” Übersetzungsscheme: Ersetze jeden Typparameter durch Object.

Beispiel

// requires C++11

#include <iostream>
#include <string>
using namespace std;



// C++ Templates und Vergleich zu Java generics
/////////////////////////////////////////////////


/////////////////////////////
// Templates

template<typename T>
void mySwap(T& x, T&y) {
  T tmp;

  tmp = x; x = y; y = tmp;
}

void testTmp() {
  int x = 1;
  int y = 2;

  mySwap<int>(x,y); // Instanzierung


  float u = 1.0;
  float v = 2.0;

  mySwap(u,v); // Instanz <float> kann inferriert werden mit Hilfe der Argumente.

}

/*

C++ verwendet "Monomorphisation".

 - Für jede Instanz dupliziere den Programmcode

 - Vorteil. Effizient, da keine Typecasts
   notwendig sind. Typ-spezifischen Optimierungen etc.

 - Nachteil. Codeduplikation

Hier am Beispiel von oben.
*/

void mySwap_int(int& x, int&y) {
  int tmp;

  tmp = x; x = y; y = tmp;
}

void testTmp_mono() {
  int x = 1;
  int y = 2;

  mySwap_int(x,y);

}



// Weiteres Template Beispiel.

template<typename T>
class Elem {
  T val;
public:
  Elem(T init) {
    val = init;
  }
  void print() {
    cout << "\n" << val;
  }
  void replace(T x) {
    val = x;
  }
};

void testElem() {
  Elem<int> e(1);
  Elem<string> s("Hello");

  e.print();
  e.replace(2);
  e.print();
  s.print();
  s.replace("Hallo");
  s.print();

}

// Beachte.
// Kein Typchecking von templates!
// Nur Typchecking von Code erhalten
// durch Monomorphisierung.

struct Point {
  int x;
  int y;
};


void testElem2() {
  Elem<struct Point> p({1,2}); // (P)

  // Kein Typfehler.
  // Erst durch Hinzunahme folgender Codezeile.

  // p.print();

}



// Beachte:
// Templates sind verschieden von "generics" in Java.
// In C++
//  - Monomorpization
// In Java
//  - Generische Uebersetzung

// Java's generisches Uebersetzungs Schema am Beispiel.

// 1. Ersetze alle Typparameter durch void*.
// (In Java wird Object verwendet).

class Elem_G {
  void* val;
public:
  Elem_G(void* init) {
    val = init;
  }
  void print() {
    cout << "\n" << val;
  }
  void replace(void* x) {
    val = x;
  }
};

// Passt alles?

void testElem_G() {
  int i = 1;
  Elem_G e(&i);
  // int* <= void*

  e.print();
  int j = 2;
  e.replace(&j);
  e.print();
}


// Beachte.
// Keine korrekte Behandlung von print.
// Benoetigen "run-time type info"
// im Falle von "<<"



enum TYPE {
  INT,
  BOOL,
  // and so on
};

class Elem_GG {
  void* val;
  enum TYPE t;
public:
  Elem_GG(void* init, enum TYPE ty) {
    val = init;
    t = ty;
  }
  void print() {
    // Entspricht "instanceof" in Java.
    switch (t) {
      case INT: {
    int* p = (int*)(val);
          cout << "\n" << *p;
          break;
      }
      case BOOL: {
    bool* p = (bool*)(val);
          cout << "\n" << *p;
          break;
      }
    }
  }
  void replace(void* x) {
    val = x;
  }
};


void testElem_GG() {
  int i = 1;
  Elem_GG e(&i, INT);
  // int* <= void*

  e.print();
  int j = 2;
  e.replace(&j);
  e.print();

  bool b = true;
  Elem_GG f(&b,BOOL);
  f.print();
}

/*

Zusammengefasst.

Monomorphization in C++
   + Effizient
   - Code Duplikation

Generische Uebersetzung in Java
   + Generischer Code
   +- Typchecks zur Laufzeit sind notwendig

 */



int main() {
  // testAdd();
  testElem();
  testElem_G();
  testElem_GG();
}