C++ Dynamic Binding
C++ Dynamic Binding
Schmidt
Motivation
When designing a system it is often the case that developers:
1. Know what class interfaces they want, without precisely knowing the most suitable representation 2. Know what algorithms they want, without knowing how particular operations should be implemented In both cases, it is often desirable to defer certain decisions as long as possible Goal: reduce the effort required to change the implementation once enough information is available to make an informed decision
Copyright c 1997-2006
Vanderbilt University
Douglas C. Schmidt
Douglas C. Schmidt
Motivation (contd)
Therefore, it is useful to have some form of abstract place-holder Information hiding & data abstraction provide compile-time & link-time place-holders i.e., changes to representations require recompiling and/or relinking... Dynamic binding provides a dynamic place-holder i.e., defer certain decisions until run-time without disrupting existing code structure Note, dynamic binding is orthogonal to dynamic linking... Dynamic binding is less powerful than pointers-to-functions, but more comprehensible & less error-prone i.e., since the compiler performs type checking at compile-time
Copyright c 1997-2006 Vanderbilt University 2
Motivation (contd)
Dynamic binding allows applications to be written by invoking general methods via a base class pointer, e.g.,
class Base { public: virtual int vf (void); }; Base *bp = /* pointer to a subclass */; bp->vf ();
However, at run-time this invocation actually invokes more specialized methods implemented in a derived class, e.g.,
class Derived : public Base { public: virtual int vf (void); }; Derived d; bp = &d; bp->vf (); // invokes Derived::vf()
In C++, this requires that both the general and specialized methods are virtual methods
Copyright c 1997-2006 Vanderbilt University 3
Douglas C. Schmidt
Douglas C. Schmidt
Motivation (contd)
Dynamic binding facilitates more exible and extensible software architectures, e.g., Not all design decisions need to be known during the initial stages of system development i.e., they may be postponed until run-time Complete source code is not required to extend the system i.e., only headers & object code This aids both exibility & extensibility Flexibility = easily recombine existing components into new congurations Extensibility = easily add new components
Copyright c 1997-2006
Vanderbilt University
Copyright c 1997-2006
Vanderbilt University
Douglas C. Schmidt
Douglas C. Schmidt
Copyright c 1997-2006
Vanderbilt University
Copyright c 1997-2006
Vanderbilt University
Douglas C. Schmidt
Douglas C. Schmidt
Note, virtual methods must be class methods, i.e., they cannot be: Ordinary stand-alone functions class data Static methods Other languages (e.g., Eiffel) make dynamic binding the default... This is more exible, but may be less efcient
Copyright c 1997-2006
Vanderbilt University
Copyright c 1997-2006
Vanderbilt University
Douglas C. Schmidt
Douglas C. Schmidt
Virtual methods have a xed interface, but derived implementations can change, e.g.,
struct Derived_1 : public Base { virtual int vf1 (void) { cout << "world\n"; } };
Supplying virtual keyword is optional when overriding vf1() in derived classes, e.g.,
struct Derived_2 : public Derived_1 { int vf1 (void) { cout << "hello world\n"; } // Still virtual int f1 (void); // not virtual };
e.g.,
void foo (Base *bp) { bp->vf1 (); /* virtual */ } Base b; Base *bp = &b; bp->vf1 (); // prints "hello" Derived_1 d; bp = &d; bp->vf1 (); // prints "world" foo (&b); // prints "hello" foo (&d); // prints "world"
Copyright c 1997-2006
Vanderbilt University
11
Douglas C. Schmidt
Douglas C. Schmidt
Shape Example
The canonical dynamic binding example: Describing a hierarchy of shapes in a graphical user interface library e.g., Triangle, Square, Circle, Rectangle, Ellipse, etc. A conventional C solution would 1. Use a union or variant record to represent a Shape type 2. Have a type tag in every Shape object 3. Place special case checks in functions that operate on Shapes e.g., functions that implement operations like rotation & drawing
e.g.,
void foo (Base *bp) { bp->vf1 (); // Actual call // (*bp->vptr[1])(bp); }
Using virtual methods adds a small amount of time & space overhead to the class/object size and method invocation time
Copyright c 1997-2006 Vanderbilt University 12 Copyright c 1997-2006 Vanderbilt University 13
Douglas C. Schmidt
Douglas C. Schmidt
Copyright c 1997-2006
Vanderbilt University
15
Douglas C. Schmidt
Douglas C. Schmidt
Copyright c 1997-2006
Vanderbilt University
16
Copyright c 1997-2006
Vanderbilt University
17
Douglas C. Schmidt
Douglas C. Schmidt
Circle
Triangle
Rectangle
Color
1
Point
1 1 1
Shape
A
Copyright c 1997-2006
Vanderbilt University
18
Copyright c 1997-2006
Vanderbilt University
19
Douglas C. Schmidt
Douglas C. Schmidt
Copyright c 1997-2006
Vanderbilt University
20
Copyright c 1997-2006
Vanderbilt University
21
Douglas C. Schmidt
Douglas C. Schmidt
Copyright c 1997-2006
Vanderbilt University
22
Copyright c 1997-2006
Vanderbilt University
23
Douglas C. Schmidt
Douglas C. Schmidt
Copyright c 1997-2006
Vanderbilt University
25
OO Progra
Douglas C. Schmidt
draw
This code works regardless of what Shape subclass sp actually points to, e.g.,
rotate
The C++ solution associates specializations with derived classes, rather than with function rotate shape() Its easier to add new types without breaking existing code since most changes occur in only one place, e.g.:
class Square : public Rectangle { // Inherits length & width from Rectangle public: Square (Point &p, double base); virtual void rotate (double degree) { if (degree % 90.0 != 0) // Reuse existing code Rectangle::rotate (degree); } /* .... */ };
vtable (Rectangle)
Rectangle
vptr
rotate
draw
Circle c; Rectangle r;
Copyright c 1997-2006
Douglas C. Schmidt
vtable (Circle)
Circle
vptr
Copyright c 1997-2006
Vanderbilt University
27
Douglas C. Schmidt
Douglas C. Schmidt
Copyright c 1997-2006
Vanderbilt University
28
Copyright c 1997-2006
Vanderbilt University
29
Douglas C. Schmidt
Douglas C. Schmidt
vec[i]->rotate (angle) is a virtual method call It is resolved at run-time according to the actual type of object pointed to by vec[i] i.e.,
vec[i]->rotate (angle) becomes (*vec[i]->vptr[1]) (vec[i], angle);
Copyright c 1997-2006
Vanderbilt University
30
Copyright c 1997-2006
Vanderbilt University
31
Douglas C. Schmidt
Douglas C. Schmidt
Shape *shapes[] = { new Circle (/* .... */), new Square (/* .... */) }; int size = sizeof shapes / sizeof *shapes; rotate_all (shapes, size, 98.6);
vptr Circle
vptr Square
Note, it is not generally possible to know the exact type of elements in variable shapes until run-time However, at compile-time we know they are all derived subtypes of base class Shape This is why C++ is not fully polymorphic, but is strongly typed
shapes
Copyright c 1997-2006
Vanderbilt University
32
Copyright c 1997-2006
Vanderbilt University
33
Douglas C. Schmidt
Douglas C. Schmidt
Calling Mechanisms
Given a pointer to a class object (e.g., Foo *ptr) how is the method call ptr->f (arg) resolved? There are three basic approaches: 1. Static Binding 2. Virtual Method Tables 3. Method Dispatch Tables C++ & Java use both static binding & virtual method tables, whereas Smalltalk & Objective C use method dispatch tables Note, type checking is orthogonal to binding time...
Copyright c 1997-2006
Vanderbilt University
34
Copyright c 1997-2006
Vanderbilt University
35
Douglas C. Schmidt
Douglas C. Schmidt
Static Binding
Method fs address is determined at compile/link time Provides for strong type checking, completely checkable/resolvable at compile time Main advantage: the most efcient scheme e.g., it permits inline method expansion Main disadvantage: the least exible scheme
Copyright c 1997-2006
Vanderbilt University
36
Copyright c 1997-2006
Vanderbilt University
37
Douglas C. Schmidt
OO Progra
Main advantages 1. More exible than static binding 2. There only a constant amount of overhead (compared with method dispatching) 3. e.g., in C++, pointers to functions are stored in a separate table, not in the object! Main disadvantages Less efcient, e.g., often not possible to inline the virtual method calls...
class Foo { public: virtual int f1 (void); virtual int f2 (void); int f3 (void); private: // data ... }; Foo obj_1, obj_2, obj_3;
vtable
obj 2
vptr
Copyright c 1997-2006
Vanderbilt University
38
Copyright c 1997-2006
Douglas C. Schmidt
obj 1
vptr
e.g.,
Vanderbilt University
f1
obj 3
vptr
Douglas C. Schmidt
Douglas C. Schmidt
Downcasting
Downcasting is dened as casting a pointer or reference of a base class type to a type of a pointer or reference to a derived class i.e., going the opposite direction from usual base-class/derived-class inheritance relationships... Downcasting is useful for 1. Cloning an object e.g., required for deep copies 2. Restoring an object from disk This is hard to do transparently... 3. Taking an object out of a heterogeneous collection of objects & restoring its original type Also hard to do, unless the only access is via the interface of the base class
Copyright c 1997-2006 Vanderbilt University 41
Douglas C. Schmidt
Douglas C. Schmidt
Contravariance
Downcasting can lead to trouble due to contravariance, e.g.:
struct Base { int i_; virtual int foo (void) { return i_; } }; struct Derived : public Base { int j_; virtual int foo (void) { return j_; } }; void foo (void) { Base b; Derived d; Base *bp = &d; // "OK", a Derived is a Base Derived *dp = &b;// Error, a Base is not necessarily a Derived }
Contravariance (contd)
dp bp
b i
d i
Copyright c 1997-2006
Vanderbilt University
42
Copyright c 1997-2006
Vanderbilt University
43
Douglas C. Schmidt
Douglas C. Schmidt
Contravariance (contd)
Since a Derived object always has a Base part certain operations are ok:
bp = &d; bp->i_ = 10; bp->foo (); // calls Derived::foo ();
Since base objects dont have subclass data some operations arent ok e.g., accesses information beyond end of b:
dp = (Derived *) &b; dp->j_ = 20; // big trouble!
Douglas C. Schmidt
Douglas C. Schmidt
e.g.,
void clone (Base &ob1) { try { Derived &ob2 = dynamic_cast<Derived &>(ob1); /* ... */ } catch (bad_cast) { /* ... */ } }
For a dynamic cast to succeed, the actual type of b would have to either be a Derived object or some subclass of Derived if the types do not match the operation fails at run-time if failure occurs, there are several ways to dynamically indicate this to the application: To return a NULL pointer for failure To throw an exception e.g., in the case of reference casts...
Copyright c 1997-2006 Vanderbilt University 46
Copyright c 1997-2006
Vanderbilt University
47
Douglas C. Schmidt
Douglas C. Schmidt
e.g.,
typeid (type_name) yields const Type_info & typeid (expression) yields const Type_info &
typeid (*bp) == typeid (Derived) // true typeid (*bp) == typeid (Base) // false typeid (br) == typeid (Derived) // true typeid (br) == typeid (Base) // false typeid (&br) == typeid (Base *) // true typeid (&br) == typeid (Derived *) // false
Note that the expression form returns the run-time type of the expression if the class is dynamically bound...
Copyright c 1997-2006
Vanderbilt University
48
Copyright c 1997-2006
Vanderbilt University
49
Douglas C. Schmidt
Douglas C. Schmidt
Summary
Dynamic binding enables applications & developers to defer certain implementation decisions until run-time i.e., which implementation is used for a particular interface It also facilitates a decentralized architecture that promotes exibility & extensibility e.g., it is possible to modify functionality without modifying existing code There may be some additional time/space overhead from using dynamic binding... However, alternative solutions also incur overhead, e.g., the union/switch approach
50 Copyright c 1997-2006 Vanderbilt University 51
This style programming leads to an alternative, slower method of dispatching methods i.e., duplicating vtables in an unsafe manner a compiler cant check
Copyright c 1997-2006 Vanderbilt University