CPSC 427: Object-Oriented Programming
Michael J. Fischer
Functions Revisted (continued)
Functional
composition
Functional composition refers to using the result returned by one
function as the argument for another.
Example: g(f(x)).
The type of f(x) (which is the result type declared in the definition of
f()) must be compatible with the corresponding parameter type for
some method of g().
Types are compatible if they are the same, or if the result type can be converted to the corresponding parameter type.
Type compatibility
Here’s what the compiler does when it sees the call g(f(x)).
Calling constructors implicitly
Normally, constructors are called implicitly when an object is created,
whether by new (in the case of dynamic storage) or by having a
declaration executed (in the case of automatic storage).
When several constructor methods are present, which is chosen depends on the arguments supplied, either explicity or through ctors, but the call itself is implicit.
Examples
Calling constructors explicitly
Constructors can also be called explicitly, just like ordinary global
functions.
The meaning is to create a new temporary stack object, just as a new
temporary is created to hold the result of y+z in the expression
x*(y+z).
As with all object construction, the constructor is called when the object
is created, and the destructor is called when it is deleted.
Because the created object is temporary, it must be used immediately,
after which it will be discarded.
This is how throw Fatal("Error message") works. Fatal() creates an exception object of type Fatal for use by throw.
Conversion using constructor
Now suppose f() returns an object of type A& and g() expects an
argument of type B. What happens with g(f())?
Example 1:
Compiler will use B’s constructor to build a B& from an A&.
Output is “B constructor called”.
Conversion using a cast
Example 2:
Compiler will use A::operator B() to cast the A& returned by f() to the B expected by g().
Output is “operator B cast called”.
What if both options exist?
Compiler will complain “error: conversion from ’A’ to ’B’ is ambiguous”.
Polymorphic Derivation
Some uses for derived classes.
Type Hierarchies
Consider following simple type hierarchy:
We have a base class B and derived classes U and V.
A different method f() is defined in each.
Relationships: A U is a B (and more). A V is a B (and more).
A U can be used wherever a B is expected.
Example: Definition f(B& x) ... ; call U z; f(z);
Inside of f(), only the B-part of z is visible. This is called slicing.
Pointers and slicing
Declare B* bp; U* up = new U; V* vp = new V.
Can write bp = up; or bp = vp;.
Why does this make sense?
If bp = up, then bp points to the embedded B-instance of object *up. The rest of *up is inaccessible because of object slicing.
Ordinary derivation
In our previous example
bp can point to objects of type B, type U, or type V.
Want bp->f() to refer to U::f() if bp points to a U object.
Want bp->f() to refer to V::f() if bp points to a V object.
However, with ordinary derivation, bp->f() always refers to B::f().
Polymorphic derivation
The keyword virtual allows for polymorphic derivation.
A virtual function is dispatched at run time to the class of the actual
object.
bp->f() refers to U::f() if bp points to a U.
bp->f() refers to V::f() if bp points to a V.
bp->f() refers to B::f() if bp points to a B.
Here, the type refers to the allocation type.
Unions and type tags
We can regard bp as a pointer to the union of types B, U and
V.
To know which of B::f(), U::f() or V::f() to use for the call
bp->f() requires runtime type tags.
If a class has virtual functions, the compiler adds a type tag field to each object.
This takes space at run time.
The compiler also generates a vtable to use in dispatching calls on virtual functions.
Virtual destructors
Consider delete bp;, where bp points to a U but has type B*.
The U destructor will not be called unless destructor B::~B() is declared
to be virtual.
Note: The base class destructor is always called, whether or not it is
virtual.
In this way, destructors are different from other member methods.
Conclusion: If a derived class has a non-empty destructor, the base class destructor should be declared virtual.
Uses of polymorphism
Some uses of polymorphism:
Multiple representations
Might want different representations for an object.
Example: A point in the plane can be represented by either Cartesian or
Polar coordinates.
A Point base class can provide abstract operations on points.
E.g., virtual int quadrant() const returns the quadrant of
*this.
For Cartesian coordinates, quadrant is determined by the signs of the x and y coordinates of the point.
For polar coordinates, quadrant is determined by the angle θ.
Both Cartesian and Polar derived classes should contain a method for int quadrant() const.
Heterogeneous containers
One might wish to have a stack of Point objects.
The element type of the stack would be Point*.
The actual values would have type either Cartesian* or Polar*.
The automatically generated type tags and dynamic dispatching obviates
the need to cast the result of pop() to the correct type.
Example:
Stack st; Point* p; |
p = st.pop(); | // no need to cast result |
p->quadrant(); | // automatic dispatch |