Reflection in C++

Runtime Type Information in C++

Recently, type_info, a meta-class that provides a limited amount of somewhat unreliable runtime type information, was added to the standard C++ library. The type_info class includes member functions for discovering the name of the type (name and raw_name), for comparing types (== and !=), and for determining the order of types in collating sequences (collating sequences are used by objects representing locales):

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 standard C++ library also provides a global operator named typeid() that expects any expression as input and returns a constant reference to a type_info object representing the expression's type:

const type_info& typeid(exp);

For example, assume we define the C++ version of our Note class:

class Note
{
public:
   Note()
   {
      frequency = 60;
      duration = 300;
   }
   virtual void play()
   {
      cout << "playing a generic note" << endl;
   }
private:
   int frequency; // in Hz
   int duration;  // in millisecs
};

As in our Java version, HornNote and ViolinNote are defined as subclasses:

class HornNote: public Note
{
public:
   void play()
   {
      cout << "playing a horn note" << endl;
   }
};

class ViolinNote: public Note
{
public:
   void play()
   {
      cout << "playing a violin note" << endl;
   }
};

For testing purposes, we introduce a global function that displays the name of the type of note pointed at by its parameter:

void displayType(Note* note)
{
   const type_info& tp = typeid(*note);
   cout << "type = " << tp.name() << endl;
}

Notice that the value returned by typeid() must be stored in a constant reference variable.

Our test driver, main(), passes the same note pointer to displayType() three times. However, each time the pointer points to a different type of Note object:

Note* note = new Note();
displayType(note);

note = new HornNote();
displayType(note);

note = new ViolinNote();
displayType(note);

To get main() and displayType() to compile, we need the following include directives:

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

Some compilers generate the necessary type information by default. To save space, other compilers, such as VC++, require programmers to set special compiler options before the necessary type information will be generated.[1]

It should also be mentioned that distinct type information for a subclass is generated by the typeid() operator only if the base class is polymorphic (i.e., contains at least one virtual function). Otherwise, the typeid() operator simply produces the type_info object of the base class.

Unfortunately, the output produced by the test program is compiler-dependent. The output produced by DJGPP, a GNU compiler for Windows, prefaces the type names by their lengths:

type = 4Note
type = 8HornNote
type = 10ViolinNote

The output produced by VC++ prefaces type names with the string "class":

type = class Note
type = class HornNote
type = class ViolinNote

The typeid() operator can also convert a type expression into its associated type_info object.

const type_info& typeid(type-exp);

We can compare this object to the type_info object of an expression using the == operator. This could be used to perform safe downcasts. For example, assume we add a special honk() function to our HornNote class:

class HornNote: public Note
{
public:
   void honk()
   {
      cout << "HONK, HONK!\n";
   }
   // etc.
};

Assume a test program uses a generic Note pointer to point at a HornNote object:

Note* note = 0;
// and later:
note = new HornNote();

Now assume we want to call the honk() function. An explicit downcast will be required, but we want to be sure note points to a HornNote before proceeding. Here's one way to do this:

if (typeid(*note) == typeid(HornNote))
   ((HornNote*)note)->honk();
else
   cerr << "Error: unexpected type\n";

Of course this is exactly what the dynamic_cast<> operator does:

HornNote* hornNote = dynamic_cast<HornNote*>(note);
if (hornNote)
   hornNote->honk();
else
   cerr << "Error: unexpected type\n";

Reflection in MFC

Of course we could try to imitate Java in C++ by introducing our own meta classes. In fact, we can find meta classes in several proprietary C++ libraries, including Microsoft Foundation Classes (MFC), which is a framework for developing Windows applications in C++. Almost all MFC classes derive from the CObject[2] base class, which is roughly similar in purpose to Java's Object base class:

class CObject
{
public:
   virtual CRuntimeClass* GetRuntimeClass() const;
   bool IsKindOf(const CRuntimeClass* pClass) const;
   // etc.
};

Any CObject-derived class, A, will redefine the virtual GetRuntimeClass() function, which returns an instance of CRuntimeClass that represents A. MFC's CRuntimeClass is roughly similar to Java's Class class:

class CRuntimeClass
{
public:
   char* m_lpszClassName; // = class name
   CObject* CreateObject();
   bool IsDerivedFrom(const CRuntimeClass* pBaseClass) const;
   // etc.
};

Example

Returning once again to our musical example, let's redefine Note as a subclass of MFC's CObject base class:

class Note: public CObject
{
public:
   DECLARE_DYNCREATE( Note )
   Note()
   {
      frequency = 60;
      duration = 300;
   }
   virtual void play()
   {
      cout << "playing a generic note" << endl;
   }
private:
   int frequency; // in Hz
   int duration;  // in millisecs
};

DECLARE_DYNCREATE is one of many macros provided by MFC. This one expands into the declarations of the member functions needed to support reflection. The macro must also appear in the subclasses of Note:

class HornNote: public Note
{
public:
   DECLARE_DYNCREATE( HornNote )
   void play()
   {
      cout << "playing a horn note" << endl;
   }
};

class ViolinNote: public Note
{
public:
   DECLARE_DYNCREATE( ViolinNote )
   void play()
   {
      cout << "playing a violin note" << endl;
   }
};

We place the macros that expand into the implementations of these member functions in the corresponding implementation file (e.g., note.cpp):

IMPLEMENT_DYNCREATE( Note, CObject )
IMPLEMENT_DYNCREATE( HornNote, Note )
IMPLEMENT_DYNCREATE( ViolinNote, Note )

Here are some of the include directives we will need:

#include <afx.h> // MFC library
#include <iostream>
using namespace std;

We will also need to tell the linker to link the MFC library with our program.[3] (Of course the MFC library is only available in certain integrated development environments such as Visual C++.)

Our test driver, main(), begins by creating a Note object, then printing the name of its class:

Note* note = new Note();
CRuntimeClass *rtclass = note->GetRuntimeClass();
cout << "class = " << rtclass->m_lpszClassName << endl;

Here's the output produced:

class = Note

Next, we point the note pointer at two other notes and print the class name:

note = new HornNote();
rtclass = note->GetRuntimeClass();
cout << "class = " << rtclass->m_lpszClassName << endl;
  
note = new ViolinNote();
rtclass = note->GetRuntimeClass();
cout << "class = " << rtclass->m_lpszClassName << endl;

Here's the output produced:

class = HornNote
class = ViolinNote

We can also ask if an object belongs to other classes. We form these classes using the RUNTIME_CLASS macro:

cout << "Is CObject? = ";
cout << note->IsKindOf(RUNTIME_CLASS(CObject)) << endl;
cout << "Is HornNote? = ";
cout << note->IsKindOf(RUNTIME_CLASS(HornNote)) << endl;

Here's the output produced:

Is CObject? = 1
Is HornNote? = 0

Reflection in MFC goes beyond runtime type identification. For example, we can dynamically create objects from meta objects.

rtclass = RUNTIME_CLASS(HornNote);
note = (Note*)rtclass->CreateObject();
cout << "class = " << rtclass->m_lpszClassName << endl;

Here's the output produced:

class = HornNote



[1] 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

[2] MFC names usually begin with a letter that indicates their type. CObject is the name of a class, so it begins with "C".

[3] In VC++ select Settings from the Project menu. In the General page select "Use MFC in a shared DLL" in the Microsoft Foundation Classes drop down list.