GO
home account info subscribe login search FAQ/help site map contact us


 
Brief Full
 Advanced
      Search
 Search Tips
[an error occurred while processing this directive]
Previous Table of Contents Next


APPENDIX B
RUNTIME TYPE INFORMATION

One of the fundamental principles of object-oriented programming is polymorphism, which is the ability of different objects to react in an individual manner to the same message. Polymorphism is widely used in natural languages. Consider the verb “to close.” It means different things when applied to different objects. Closing a door, closing a bank account, and closing a program’s window are all different actions; the exact meaning of closing depends on the object on which the action is performed. Similarly, polymorphism in object-oriented programming means that the interpretation of a message depends on its object. In C++, polymorphism is implemented via static and dynamic binding. Dynamic binding is a delayed resolution of which function is invoked. The resolution in dynamic binding is delayed until runtime. Static binding, on the other hand, resolves function calls at compile-time.

STATIC BINDING

C++ has two mechanisms for implementing static polymorphism:

  Operator overloading. Applying operator +=, for example, to an int or a string is interpreted by each of these objects in an individual manner. But the results can be predicted intuitively, and some similarities can be found between the two.
  Templates. A vector<int> and a vector<string> react differently; that is, they execute a different set of instructions when they receive the same message. However, close behaviors can be expected. For example:
vector < int > vi; vector < string > names;
string name(“Bjarne”);
vi.push_back( 5 ); // add an integer at the end of the vector
names.push_back (name); //add a string at the end of the vector

DYNAMIC BINDING

Dynamic binding takes the notion of polymorphism one step further. In dynamic binding, the meaning of a message depends on the object, but the exact type of the object can be determined only at runtime. Virtual member functions are a good example of that. The specific version of a virtual function is unknown at compile time. Therefore, the call resolution is delayed until runtime. Here is an example:

#include <iostream>
using namespace std;

class base{
  public: virtual void f() { cout<< “base”<<endl;}
};

class derived : public base{
  public: void f() { cout<< “derived”<<endl;} //overrides base::f
};

void identify(base & b) { // the argument can be an instance of base or any
			              // object derived from it
  b.f(); // which f should be invoked base::f or derived::f? resolution is
	   // delayed to runtime
}
void main()
{
  derived d;
  identify(d); // argument is an object derived from base
}

Please note that the function identify can receive any object derived from class base, even objects of subclasses that were defined after identify was compiled.

Dynamic binding has enormous advantages. In this example, it enables the user to extend the functionality of base without modifying identify in any way. In procedural programming, such flexibility is impossible. Furthermore, the underlying mechanism of dynamic binding is automatic. The programmer doesn’t need to implement the code for runtime lookup and dispatch of a virtual function, nor does he or she need to check the dynamic type of the object. Still, under some circumstances, detecting of the dynamic type of an object is unavoidable. In this appendix, you will learn when and how runtime type information (RTTI) is used.

Virtual Functions

Suppose you have to develop a file manager application that is a component of a GUI-based operating system. The files in the system are represented as icons that respond to a right-click of a mouse, and display a menu with options such as open, close, read, and so on. The underlying implementation of the file system relies on a class hierarchy that represents files of various types. A well-designed class hierarchy usually has an abstract class serving as an interface:

class File { //abstract
  public: virtual void open() =0; //pure virtual member function
  public: virtual void read() =0;
  public: virtual void write() =0;
  public: virtual ~File () =0;
};

At a lower level in the hierarchy is a set of derived classes that implement the common interface. Each of these classes represents a different family of files. To simplify the discussion, let’s assume there are only two file types in the system: binary .exe files and text files.

class BinaryFile : public File {
public:
  void open () { OS_execute(this); }  //implement the pure virtual function
  //...other member functions
};
class TextFile : public File {
public:
 void open () { Activate_word_processor (); } //implement pure virtual function
 //other member functions of File are implemented here
 void virtual print();  // an additional member function
};

The pure virtual function open is implemented in every derived class according to the type of the file. Therefore, a TextFile object activates a word processor, whereas a BinaryFile object invokes the operating system’s API function OS_execute, which in turn executes the program stored in the binary file.

There are several differences between a binary file and a text file. For example, a text file can be printed directly on a screen or a printer because it consists of a sequence of printable characters. Conversely, a binary file with an .exe extension contains a stream of bits. Therefore, such a file cannot be printed or displayed on a screen directly. It must be converted to a text file first, usually by a utility that translates the binary data into its symbolic representations. For instance, the sequence 0110010 can be replaced by a corresponding move esp, ebp assembly directive. Therefore, the member function print is declared only in class TextFile.

In this file manager, a right-click of a mouse on a file icon opens a menu of messages (options) to which the object can respond. For that purpose, the operating system has a function that takes a reference to a File:

OnRightClick (File & file); //operating system’s API function

Obviously, no object of class File can be instantiated because File is an abstract class. However, the function OnRightClick can accept any object derived from File. For instance, when the user right-clicks on a file icon and chooses the Open option, OnRightClick invokes the virtual member function open of its argument, and the appropriate member function is called.

OnRightClick (File & file) //operating system’s API function
{
  switch (message){
  //...
  case m_open:
    file.open();
  break;
  }
}

So far, so good. You have implemented a polymorphic class hierarchy and a generic function that does not depend on the dynamic type of its argument. In this case, the language support for virtual functions was sufficient for the purpose; no explicit RTTI was required. Well, not exactly. You might have noticed that file printing was not addressed. Let’s look at the definition of class TextFile again:

class TextFile : public File {
public:
  void open () { Activate_word_processor (); } //implement the pure virtual
                                               //function
  void virtual print();
};

The member function print is not a part of the common interface implemented by all files in this system. It would be a design error to move it to the abstract class File because binary files are nonprintable and cannot define a meaningful operation for it. Then again, OnRightClick has to support file printing when it handles a text file. In this case, ordinary polymorphism in the form of virtual member functions is insufficient. All that OnRightClick knows about its argument is that the argument is derived from File, which isn’t enough to tell whether the actual object is printable or not. Clearly, OnRightClick needs more information about the dynamic type of its argument to handle file printing properly. This is where the need for runtime type information arises.

HISTORICAL BACKGROUND

Originally, C++ did not support RTTI. Furthermore, its creators balked at the idea of adding RTTI support to C++ for at least two reasons. First, they wanted to preserve backward compatibility with C. Second, they were concerned about efficiency. Other RTTI-enabled languages, such as Smalltalk and LISP were characterized by sluggish performance. C++ designers attempted to preserve the efficiency of C. Still, it became apparent that under some circumstances, static type checking alone was insufficient. The addition of multiple inheritance (and consequently, virtual inheritance) to C++ in 1989 gave overwhelming ammunition to the proponents of RTTI.


NOTE:  

Multiple inheritance will not be discussed here. Nonetheless, it is important to note that RTTI is required when virtual inheritance is used.


Eventually, the C++ standardization committee approved the addition of RTTI to the language. Two new operators, dynamic_cast<> and typeid, were added to C++ to support RTTI. In addition, the class type_info was added to the Standard Library.

RTTI CONSTITUENTS

RTTI Is Applicable to Polymorphic Objects Exclusively

It is important to realize that RTTI is applicable to polymorphic objects solely. A class must have at least one virtual member function in order to have RTTI support for its objects. C++ does not offer RTTI support for non-polymorphic classes and primitive types. This restriction is just common sensea double or a string cannot change its type at runtime. Therefore, there is no point in detecting its dynamic typeit is identical to the static type anyway. But there is another reason for confining RTTI support to polymorphic classes. As you probably know, every object that has at least one virtual member function also contains a special data member added by the compiler. This member is a pointer to the virtual function table. The runtime type information is stored in this table as a pointer to a const type_info object.

Class type_info

For every distinct polymorphic type, C++ instantiates a corresponding RTTI object that contains the necessary runtime type information. The RTTI object is an instance of the standard class type_info (defined in the standard header <typeinfo>). The type_info object is owned by C++, and may not be altered in any way by the programmer. The implementation-independent part of type_info looks like the following (source: ANSI/ISO Final Draft International Standard of the C++ Programming Language, ISO/IEC 14882: 1998):

namespace std { //class type_info is declared in namespace std
  class type_info {
  public:
    virtual ~type_info(); //may be subclassed
    bool operator==(const type_info&  rhs ) const;  // comparison; return true
					                    // if *this == rhs
    bool operator!=(const type_info&  rhs ) const; // return !( *this == rhs)
    bool before(const type_info&  rhs ) const; // ordering
    const char* name() const; //return a const null terminated string
			          // containing the type’s name
  private:
    //objects of this type cannot be copied
	 type_info(const type_info&  rhs );
	 type_info& operator=(const type_info&  rhs );
  }; //type_info
}

In general, all instances of the same type share a single type_info object. The most widely used member functions of type_info are name and operator==. But before you can invoke these member functions, you have to access the type_info object itself. How is it done?

Operator typeid

Operator typeid takes either an object or a class name as its argument and returns a matching const type_info object. The dynamic type of an object can be examined like this:

OnRightClick (File & file) //operating system’s API function
{
  if ( typeid( file)  == typeid( TextFile ) )
  {
    //we received a TextFile object; printing should be enabled
  }
  else
  {
    //not a TextFile object, printing is not supported
  }
}

To understand how it works, let’s look at the highlighted source line

if ( typeid( file)  == typeid( TextFile ) ).

The if statement tests whether the dynamic type of the argument file is TextFile (the static type of file is File, of course). The leftmost expression typeid(file) returns a type_info object that holds the necessary runtime type information associated with the object file. The rightmost expression typeid(TextFile) returns the type information associated with class TextFile. Of course, when typeid is applied to a class name (rather than an object) it always returns the type_info object that corresponds to that class name. As you saw earlier, type_info overloads the operator ==. Therefore, the type_info object returned by the leftmost typeid expression is compared to the type_info object returned by the rightmost typeid expression. If indeed file is an instance of TextFile, the if statement evaluates to true. In this case, OnRightClick displays an additional option in the menu: print. If, on the other hand, file is not a TextFile, the if statement evaluates to false, and the print option will not be displayed.

This is all nice and well, but a typeid-based solution incurs a drawback. Suppose you want to add support for a new type of file, say an HTML file. What happens when the file manager application has to be extended? HTML files are essentially text files. They can be read and printed. However, they differ from plain text files in some respects. An open message applied to an HTML file launches a browser rather than a word processor. In addition, HTML files have to be converted to a printable format before they can be printed. The need to extend a system’s functionality at a minimal cost is an everyday challenge that software developers face. Object-oriented programming and design can facilitate the task. By subclassing TextFile, you can reuse its existing behavior and implement only the additional functionality required for HTML files:

class HTMLFile : public TextFile {
  void open () { Launch_Browser (); } //override TextFile::open
  void virtual print();  // perform the necessary conversions to a printable
		            // format and then print file
};

This is only half of the story. OnRightClick will fail badly when it receives an object of type HTMLFile. Let’s look at it again to see why:

OnRightClick (File & file) //operating system’s API function
{
  if ( typeid( file)  == typeid( TextFile ) )
  {
    //we received a TextFile object; printing should be enabled
  }
  else //OOPS! we get here when file is of type HTMLFile
  {
  }
}

typeid returns the exact type information of its argument. Therefore, the if statement in OnRightClick will evaluate to false when the argument is an HTMLFile. However, a false value implies a binary file. Consequently, no support for printing is available. This onerous bug is likely to occur every time support is added for a new file type. Fortunately, C++ offers a better way to handle this situation.

Operator dynamic_cast<>

It is a mistake to let OnRightClick take care of every conceivable class type. By doing that, you are forced to modify it whenever you add a new file class or modify an existing class. In software design in general, and object-oriented design in particular, such dependencies should be minimized. If you examine OnRightClick closely, you will realize that it shouldn’t really know whether its argument is an instance of class TextFile (or any other class for that matter). Rather, all OnRightClick needs to know is whether its argument is a TextFile. There is a big difference between the two; OnRightClick has to know whether its argument is a TextFile object or an instance of a class derived from TextFile. However, typeid is incapable of examining the derivation hierarchy of an object. For this purpose, you have to use the operator dynamic_cast<>. dynamic_cast<> takes two arguments. The first is a type name. The second argument is a polymorphic object, which dynamic_cast<> attempts to cast at runtime to the desired type. For example:

dynamic_cast <TextFile &> (file); //attempt to cast file to a reference to an
				        // object of type TextFile

If the attempted cast succeeds, either the second argument is an instance of the class name that appears as the second argument, or it is an object derived from it. The dynamic_cast<> expression above will succeed if file is a TextFile. This is exactly what OnRightClick needs to know to operate properly. How do you know whether dynamic_cast<> was successful?

There are two flavors of dynamic_cast<>. One uses pointers and the other uses references. Accordingly, dynamic_cast<> returns a pointer or a reference of the desired type when it succeeds. When dynamic_cast<> cannot perform the cast, it returns a null pointer, or in the case of a reference, dynamic_cast<> throws an exception of type std::bad_cast. Let’s look at a pointer cast example:

TextFile * pTest = dynamic_cast < TextFile *> (&f); //attempt to cast file
						             // address to a pointer to
						            // TextFile
if (pTest) //dynamic_cast succeeded, file is-a TextFile
{
}
else // file is not a TextFile;  pTest has a NULL value
{
}

C++ does not have null references. Therefore, when a reference dynamic_cast<> fails, it throws an exception of type std::bad_cast . That is why you should always place a reference dynamic_cast<> expression within a try block and include a suitable catch statement to handle std::bad_cast exceptions.

try {
 TextFile  tf = dynamic_cast < TextFile &> (f); //attempt to cast file to a
						          // reference to TextFile
//use tf safely,
}
catch (std::bad_cast) { //we get here when dynamic_cast<> fails
//
}

Now you can revise OnRightClick to handle HTMLFile objects properly:

OnRightClick (File & file) //operating system’s API function
{
  try
  {
    TextFile temp = dynamic_cast<TextFile&> (file);
    //display options, including “print”
    switch (message){
    case m_open:
      temp.open();  //virtual; either TextFile::open or HTMLFile::open is executed
    break;
    case m_print:
      temp.print();//virtual; either TextFile::print or HTMLFile::print is
                   //executed
    break;
    }//switch
  }//try
  catch (std::bad_cast& noTextFile)
  {
    // treat file as a BinaryFile; exclude“print”
  }
}// OnRightClick

The revised version of OnRightClick handles an object of type HTMLFile appropriately because an object of type HTMLFile is a TextFile. When the user clicks on the open message in the file manager application, the function OnRightClick invokes the member function open of its argument, which behaves as expected because it was overridden in class HTMLFile. Likewise, when OnRightClick detects that its argument is a TextFile, it displays a print option. If the user clicks on this option, OnRightClick will send the message print to its argument, which is supposed to react as expected.

COMMENTS

The RTTI mechanism of C++ consists of three components: operators typeid, operator dynamic_cast<>, and class type_info. RTTI is relatively new in C++. Some existing compilers do not support it yet. Furthermore, even compilers that support it can usually be configured to disable RTTI support. The reason is that RTTI adds a moderate overhead in terms of memory usage, execution speed, and the size of the executable. Even when there is no explicit usage of RTTI constructs in a program, the compiler automatically adds the necessary “scaffolding” to polymorphic objects. To avoid this, you should consult your compiler’s manual to check how you can toggle its RTTI support.

From the object-oriented design point of view, operator dynamic_cast<> is preferable to typeid because it enables more flexibility and robustness, as you have seen. However, dynamic_cast<> can be slower than typeid because it might traverse through the entire derivation tree of an object to decide whether the object can be cast to the desired type. When complex derivational hierarchies are used, the incurred performance penalty can be noticeable. Therefore, it is advisable to use RTTI judiciously. In many cases, a virtual member function is sufficient to achieve the necessary polymorphic behavior. Only when virtual member functions are insufficient, should RTTI be considered.

When using RTTI, please note the following:

  In order to enable RTTI support, an object must have at least one virtual member function. In addition, you should switch on your compiler’s RTTI support (please consult your user’s manual for further information).
  Make sure your program has a catch statement to handle std::bad_cast exception whenever you are using dynamic_cast<> with a reference.
  When using dynamic_cast<> with a pointer, always check the returned value.


Previous Table of Contents Next


Products |  Contact Us |  About Us |  Privacy  |  Ad Info  |  Home

Use of this site is subject to certain Terms & Conditions, Copyright © 1996-1999 EarthWeb Inc.
All rights reserved. Reproduction whole or in part in any form or medium without express written permision of EarthWeb is prohibited.