13. Types as Objects
A reflective system consists of two levels: a base level and the meta level:
The base level consists of base classes. A base class performs application-specific tasks. Airplane, Customer, Transaction, and Company are examples of typical base classes. Normally, users only interact with the base level of a reflective system. The meta-level consists of meta-classes. An instance of a meta-class describes the implementations and policies used by one or more base classes. By interacting with the meta-level of a reflective system, power users can dynamically inspect and negotiate changes in the implementation and behavior of the base level.
For example, the meta level of a reflective operating system might allow users to inspect and modify page swapping or process scheduling policies. The meta level of a reflective compiler might allow users to inspect and modify parameter passing mechanisms and scope rules.
Runtime Type Identification (RTTI)
Type information is used by compilers to check for inconsistencies in a program before it is translated into machine language. After the translation is performed, the type information is discarded. This is fine for most applications, but in some cases we might not be able to predict the type of data a highly polymorphic function will operate on when it is called. Instead, it will be the job of the function to begin by querying the data about its type. For example, a graphical programming environment such as Visual Basic might be expected to create and manipulate foreign components developed by third-parties in the distant future.
But what sort of answer should an object produce when asked about its class? Objects only exist at runtime, while classes only exist at compile time. Clearly, we need the ability to represent classes as objects. This sounds strange at first. We are familiar with representing application domain objects such as airplanes, customers, and pancakes as objects, but not solution domain objects such as programs, classes, and functions. What class would an object representing the Airplane or Customer class belong to? The answer: the Class class of course!
In UML a class whose members represent classes is called a meta-class. UML even provides a <<metaclass>> stereotype. We take a broader view and define a meta-class to be any class whose instances represent solution domain objects such as functions, variables, classes, statements, or programs. A meta-program is a program that manipulates meta-class objects. Compilers, debuggers, CASE tools, and optimizers are familiar examples of meta-programs.
RTTI in Java
All Java classes-user or system defined -extend the Object base class. In addition, Java provides Class, Method, and Field meta classes. Every Java object inherits from the Object base class a getClass() function that returns a reference to an appropriate Class meta-level object:
For example, assume the following Java class declarations are made:
class Document { ... }
class Memo extends Document { ... } // Memos are Documents
class Report extends Document { ... } // Reports are Documents
In other words, Memo and Report are specializations of Document:
We can think of Document, Report, and Memo as base classes and Class, Method, and Field as meta classes.
Assume x is declared as a Document reference variable:
Document x; // x can hold a reference to a document
Then we can assign references to Memo or Report objects to x:
x = new Report(); // x holds a reference to a Report object
x = new Memo(); // now x holds a reference to a Memo object
At some point in time we may be unsure if x refers to a Memo object, a Report object, or perhaps some other special type of Document object (Letter? Contract? Declaration of Independence?) This isn't a problem. Programmers can fetch x's runtime class object by calling the inherited getClass() function:
Class c = x.getClass();
For example, executing the following Java statements:
Document x = new Report();
Class c = x.getClass();
System.out.println("class of x = " + c.getName());
c = c.getSuperclass();
System.out.println("base class of x = " + c.getName());
c = c.getSuperclass();
System.out.println("base of base class of x = " + c.getName());
x = new Memo();
c = x.getClass();
System.out.println("now class of x = " + c.getName());
produces the output:
class of x = Report
base class of x = Document
base of base class of x = java.lang.Object
now class of x = Memo
Notice that the name of the Object class is qualified by the name of the package (i.e., the namespace) that it belongs to, java.lang (see Programming Note A.1.2 in Appendix 1).
Java reflection goes further by introducing Method and Field meta classes. (In Java member functions are called methods and member variables are called fields.) Let's add some fields and methods to our Document class:
class Document
{
public int wordCount = 0;
public void addWord(String s) { wordCount++; }
public int getWordCount() { return wordCount; }
}
Next, we create a document and add some words to it:
Document x = new Document();
Class c = x.getClass();
x.addWord("The");
x.addWord("End");
Executing the following Java statements:
Method methods[] = c.getMethods();
for(int i = 0; i < methods.length; i++)
System.out.println(methods[i].getName() + "()");
Field fields[] = c.getFields();
try
{
System.out.print(fields[0].getName() + " = ");
System.out.println(fields[0].getInt(x));
}
catch(Exception e)
{
System.out.println("fields[0] not an int");
}
produces the output:
getClass
hashCode
equals
toString
notify
notifyAll
wait
wait
wait
addWord
getWordCount
wordCount = 2
Notice that the methods inherited from the Object base class were included in the array of Document methods. (There are three overloaded variants of the wait() function in the Object class.) If wordCount is declared private, as it normally would be, then its value doesn't appear in the last line:
wordCount =
RTTI in C++
Recently, C++ has added runtime class objects that provide a limited amount of runtime type information (RTTI). Runtime class objects are instances of the type_info class, which is declared in typeinfo.h:
class type_info
{
public:
virtual ~type_info();
int operator==(const type_info& rhs) const;
int operator!=(const type_info& rhs) const;
int before(const type_info& rhs) const;
const char* name() const;
const char* raw_name() const;
private:
...
};
The typeid() operator returns the type of its argument:
Appliance* app = new Refrigerator(...);
type_info rtc = typeid(*app);
cout << rtc.name() << endl; // prints "class Refrigerator"
RTTI can be used to perform safe down casts and cross casts. Assume ref can hold a pointer to a refrigerator:
Refrigerator* ref;
We would like to assign app, our appliance pointer, to ref, but we are concerned that app might not actually point at a refrigerator object at the time of the assignment. The dynamic_cast() operator uses RTTI to perform safe casts. If app no longer points at a refrigerator object, the null pointer, 0, is returned:
if ( ref = dynamic_cast<Refrigerator*>(app) )
ref->setTemperature(-30);
else
cerr << "invalid cast\n";
RTTI Compiler Options
You will need to include the <typeinfo> header file to get the declaration of typeid (which is in the std namespace). Also, you may have to specify some compiler options to generate the extra typing information needed by RTTI. In VC++, select the C/C++ tab in the dialog that appears when you select Settings from the Project menu. Pick "C++ Language" in the Category combo box. Check the "Enable Runtime Type Information (RTTI)" box.
The UML Meta Model
Power types
A power type is a special kind of meta class in which instances represent subclasses of a particular class. For example, there are many types of aircraft: airplanes, blimps. helicopters, space ships, etc. We could represent this situation by introducing blimp, helicopter, and airplane as subclasses of an aircraft base class:
Alternatively, we could define a single Aircraft class and represent the different types of aircraft by different instances of an AircraftType power type. In this case it's common for instances of the power type to act as a factories that create instances of the type of objects they represent, the same way a class would provide a constructor for creating instances. It is also common for products created by a power type object to carry a type pointer back to the factory that created them:
In UML we can also represent a power type with a dashed line connecting it to the hierarchy of subclasses it represents:
Dynamic Instantiation
Instances of power types are often factories and instances of the subclasses represented by a power type instance are the products this factory creates. In C++ we can force clients to create objects by calling power type factory methods by making constructors private and by declaring the power type to be a friend class:
class Aircraft
{
public:
void takeoff() { ... }
void fly() { ... }
void land() { ... }
AircraftType* getType() { return myType; }
private:
AircraftType* myType; // = blimp, plane, saucer, etc.
Aircraft() {}
Aircraft(AircraftType* t) { myType = t; }
Aircraft(const Aircraft& a) {}
friend class AircraftType;
};
class AircraftType
{
public:
AircraftType(string n) { name = n; }
string getName() { return name; }
Aircraft* makeAircraft() { return new Aircraft(this); }
private:
string name; // = blimp, plane, saucer, etc.
};
A Java Implementation of a Power Type
How can we insure that users won't bypass the factory and directly create aircraft? This can be done in Java by defining Aircraft to be a private inner class of AircraftType. We begin by introducing an aircraft interface:
interface IAircraft {
public AircraftType getType();
public void takeoff();
public void fly();
public void land();
}
Next, we define the AircraftType class with a private inner class and a factory method:
class AircraftType {
private String name;
public AircraftType(String n) { name = n; }
public String toString() { return name; }
// factory method:
public IAircraft makeAircraft() {
return new Aircraft();
}
private class Aircraft implements IAircraft {
public void takeoff() {
System.out.println("An aircraft is taking off");
}
public void fly() {
System.out.println("An aircraft is flying");
}
public void land() {
System.out.println("An aircraft is landing");
}
public AircraftType getType() {
return AircraftType.this;
}
} // Aircraft
} // AircraftType
Here is how a client might create an aircraft:
public class TheApp {
public static void main(String[] args) {
AircraftType blimp = new AircraftType("Blimp");
AircraftType helicopter = new AircraftType("Helicopter");
//IAircraft a = blimp.new Aircraft(); // ok if inner class is public
IAircraft a = blimp.makeAircraft();
a.takeoff();
a.fly();
a.land();
System.out.println("a.getType() = "+ a.getType()); //prints Blimp
}
}
Of course reflection is built into Java, so we can use the Java meta classes Class, Field, Method, etc. without writing extra code. For example, let's drop the getType() function from the IAircraft interface:
interface IAircraft {
// public AircraftType getType();
public void takeoff();
public void fly();
public void land();
}
Assume Helicopter and SpaceShip implement this interface:
class SpaceShip implements IAircraft {
public void takeoff() {
System.out.println("A spaceship is taking off");
}
public void fly() {
System.out.println("A spaceship is flying");
}
public void land() {
System.out.println("A spaceship is landing");
}
}
Here is how a client might build and takeoff in an aircraft without knowing the type of aircraft:
import java.lang.reflect.*;
public class Test {
public static void main(String[] args) {
try {
String className = "SpaceShip";
Class c = Class.forName(className);
IAircraft a = (IAircraft)c.newInstance();
Method[] meths = c.getMethods();
for(int i = 0; i < meths.length; i++)
if (meths[i].getName().equals("takeoff"))
meths[i].invoke(a, null);
}
catch(Exception e) {
System.err.println("error = " + e);
}
}
}
Advantages and Disadvantages
In these examples we have replaced the static type system provided by C++ (and to a lesser extent Java) with a dynamic type system in which values are type tagged (this is similar to languages like LISP). Of course we loose the advantages of inheritance and polymorphism. Instead, we need to provide our own forms of inheritance and polymorphism. This can be done by providing power type objects with virtual function tables:
class AircraftType
{
public:
typedef void (*Function)(Aircraft*);
void takeoff(Aircraft* a)
{ /* search vft for takeoff and call it */ }
void fly(Aircraft* a) { /* search vft for fly and call it */ }
void land(Aircraft* a) { /* search vft for land and call it */ }
// etc.
private:
map<string, Function> vft;
};
The Aircraft member functions simply delegate to the corresponding member functions of their associated type object:
class Aircraft
{
public:
void takeoff() { myType->takeoff(this); }
void fly() { myType->fly(this); }
void land() { myType->land(this); }
AircraftType* getType() { return myType; }
private:
AircraftType* myType; // = blimp, plane, saucer, etc.
Aircraft() {}
Aircraft(AircraftType* t) { myType = t; }
Aircraft(const Aircraft& a) {}
friend class AircraftType;
};