Athena

Run-Time Type Information In C++Builder 5 - What Can It Do For You?

Brian Long (www.blong.com)

If you find this article useful then please consider making a donation. It will be appreciated however big or small it might be and will encourage Brian to continue researching and writing about interesting subjects in the future.


Introduction

All versions of Delphi and C++Builder have supported the generation of Run-Time Type Information, or RTTI. Of course C++ has defined support for RTTI for a long time, but the particular form of RTTI covered here is specific to these VCL-based RAD tools.

Support for RTTI was added first and foremost to allow the design-time environment to do its job, but developers can also take advantage of it to achieve certain code simplifications. Unfortunately, it has not been made entirely obvious how to use this information as the RTTI has, for the most part, remained undocumented. This paper attempts to lift the mystique from RTTI and move it from the realms of the propeller head into the reach of the average C++Builder user.

You can download the files that accompany this paper by clicking here.

What Is RTTI?

RTTI is implemented as data structures generated by the compiler whilst compiling a program. The general idea is to allow information describing various categories of types to be available at run-time for examination and indeed more productive purposes. Normally, types are considered to only be available at compile-time, used by the compiler to identify storage requirements, to ensure appropriate values are used with various variables and so on. RTTI means we have extra information available to query for our own purposes.

Why Do We Have RTTI?

To run the risk of stating the obvious, the C++Builder design-time environment allows you to visually design form classes that are used in your programs at run-time. It stores the form description in a binary .DFM file, including the type names of all the components placed on the form and also the names and (non-default) values of all form and component properties. At run-time your form file (which was linked into your EXE as a custom resource) is read and your EXE magically creates all the objects giving them all the required property values.

In order to get from a string, signifying a class name, to a created object of the required type requires RTTI (or a humungous case statement that would be a nightmare to maintain). Also, in order to set the values of the named properties in the created objects, bearing in mind they can be of many types, also requires RTTI.

Additionally, the dynamic_cast keyword relies on the presence of some form of RTTI to identify if a given object reference is defined in terms of a supplied class type, or one of its descendants. If no information on types was present at run-time then identifying ancestors of any types would be rather tricky.

How Do We Get RTTI?

When designing forms, references to all the objects placed on the form are inserted at the top of the form class definition in the form unit's header file, in the __published section. Entries in the published and public sections are equally accessible at run-time. The principal difference between published and public is that published properties of a component appear in the Object Inspector at design-time. This happens because RTTI is automatically generated for the types used to define data fields in the published section. The Object Inspector picks this up and uses it to identify what to add to its list of properties and events.

The form's published data fields and methods (components and event handers) have RTTI generated to enable the form and its components to be streamed to and from a form file.

To access the RTTI for a class, you can call the ClassInfo() method of an instance of that class. This returns a pointer to an RTTI block as generated by the compiler. Additionally, if there is no handy instance, you can pass your class reference to the static ClassInfo() method function defined in TObject. __typeinfo is a simple macro defined to pass the supplied class reference as a parameter to TObject::ClassInfo().

Some Comments About Delphi

In Delphi, you can also easily access RTTI for any other arbitrary type (that the compiler supports generating it for) by passing the type to the TypeInfo() function. TypeInfo() is one of those special functions that is evaluated by the Delphi compiler at compile time. If you pass a type identifier to TypeInfo() (as opposed to a class reference variable that would need to be evaluated at run-time) then, presuming it is an appropriate type, at run-time you will have some RTTI for that type to play with. If the type is a class, then the RTTI will exist in your executable anyway, but for other types such as enumerated types and sets, it won't necessarily exist.

Type information will exist for a given type if a call to TypeInfo() is made for it, or if the type is referred to in the published section of a class that is referenced in the program source. If type information exists, it will not be duplicated. Calling TypeInfo() a second time on one type will return a pointer to the same RTTI table.

Calling an object's or class's ClassInfo() method performs the same job as passing it to the TypeInfo() function. It returns a pointer to an object's RTTI. The only real difference is that TypeInfo() is evaluated at compile time. The implication of this fact is that you must pass TypeInfo() an actual type, rather than a class reference variable. The net result is that TypeInfo() will be more efficient if you know the type.

Accessing Non-Class RTTI, Part 1

As a C++Builder user, you can get RTTI for non-class types by adding a simple Delphi unit into your project. The unit can include a simple function that calls TypeInfo() and returns the RTTI block. For example, to get RTTI for the type of a form's BorderStyle property, the enumerated type TFormBorderStyle, you could make a unit like this one that would be saved as RttiUtilityUnit.Pas:

unit RTTIUtilityUnit;

interface

uses
  TypInfo;

function GetBorderStyleRTTI: PTypeInfo;

implementation

uses
  Forms;

function GetBorderStyleRTTI: PTypeInfo;
begin
  Result := TypeInfo(TFormBorderStyle)
end;

end.

If you are not familiar with Pascal syntax, then there is an alternative way to get RTTI for a type, so long as some class has a published property of the type in question (for example a form class has a BorderStyle property that would suffice). We will see this later.

How Do We Talk To RTTI?

It has been mentioned that both TObject::ClassInfo and the Delphi-specific TypeInfo() return a pointer to a compiler-generated RTTI block. In order to make use of RTTI we need to know the structure of this table. Fortunately, Borland have supplied a Pascal unit (TypInfo.Pas) and a C++ header (TypInfo.Hpp) which define it and also offer various routines to give us access to it.

The TypInfo unit defines a number of types, many of which have fields that have been commented out in the Pascal source and which are simply omitted in the C++ header. This is because the types are there to try and provide a language framework around the RTTI that the compilers generate in a space-efficient format.

Let's start at the beginning. Both the TypeInfo() pseudo-function and the ClassInfo() method are defined to return a simple void pointer, but they really return a PTypeInfo pointer. This is a pointer to the most important type in the unit, the TTypeInfo structure, which looks like this (notice that both the C++ and Pascal versions are listed, so you can see the commented out sections from the original Pascal code):

//C++Builder
struct TTypeInfo
{
  TTypeKind Kind;
  System::ShortString Name;
};

//Delphi
TTypeInfo = record
  Kind: TTypeKind;
  Name: ShortString;
  {TypeData: TTypeData}
end;

Kind is a value from the TTypeKind enumerated type. TTypeKind rather dictates the sorts of general types that can have RTTI.

TTypeKind value

VCL types that yield this value

tkUnknown

A place holder value. Never used

tkInteger

Used for any ordinal type and sub-range types

tkChar

Char and AnsiChar types (where Char and AnsiChar are synonyms)

tkEnumeration

All enumerated types. This includes Boolean, ByteBool, WordBool, LongBool and Bool

tkFloat

Any floating point type

tkString

ShortString types

tkSet

Set types

tkClass

Class types

tkMethod

Procedure and function method types

tkWChar

WideChar type

tkLString

String or AnsiString type

tkWString

WideString type

tkVariant

Variant type

tkArray

Array types

tkRecord

Record types

tkInterface

Interface types

tkInt64

Int64 type

tkDynArray

Dynamic array types (DynamicArray)

To get the type name of an appropriate type at run-time, you can access the Name field of the TTypeInfo record.

The last TTypeInfo field, TypeData, is commented out because the Name field (a string holding the type name) is not what it seems. It is implemented as a VCL short string, but for space efficiency only takes up as many bytes as are required for the type name, rather than a fixed number (which would leave several unused characters). Therefore the position of the TypeData field is variable.

If we want to get access to the TypeData field we have to fiddle around somewhat, setting up a pointer to the relevant position in the record after using the length of Name to identify where it is. The TypInfo unit provides a routine to do this as we will see later.

The TTypeData record is where things get interesting/messy (delete as appropriate). It is a large union with different fields being used depending on the value of the TTypeInfo.Kind field. Let's look at a simplified view of it.

//C++Builder
struct TTypeData
{
  union
  {
    struct //tkInterface
    {
      ...
    };
    struct //tkMethod
    {
      ...
    };
    struct //tkClass
    {
      ...
    };
    Byte MaxLength; //tkString
    TFloatType FloatType; //tkFloat
    TOrdType OrdType; //tkInteger, tkChar, tkEnumeration, tkSet, tkWChar
    union
    {
      ...
    };
  };
};

//Delphi
TTypeData = packed record
  case TTypeKind of
    tkUnknown, tkLString, tkWString, tkVariant: ();
    tkInteger, tkChar, tkEnumeration, tkSet, tkWChar: (OrdType: TOrdType; ...);
    tkFloat: (FloatType: TFloatType);
    tkString: (MaxLength: Byte);
    tkClass: (...);
    tkMethod: (...);
end;

In the case of a long or wide string or variant there is no additional type information recorded. For a floating point type, a value from the TFloatType enum appears that specifies what variety it is.

enum TFloatType { ftSingle, ftDouble, ftExtended, ftComp, ftCurr };

ftComp and ftCurr represent the VCL Comp and Currency classes respectively.

In the case of a short string, the maximum length is recorded, and for ordinal types, class types, method types and interface types, more information is stored.

//C++Builder
TOrdType OrdType; //tkInteger, tkChar, tkEnumeration, tkSet, tkWChar
union
{
  PTypeInfo *CompType;
  struct
  {
    int MinValue;
    int MaxValue;
    union
    {
      struct
      {
        PTypeInfo *BaseType;
        ShortStringBase NameList;
      };
    };
  };
};

//Delphi
tkInteger, tkChar, tkEnumeration, tkSet, tkWChar: (
  OrdType: TOrdType;
  case TTypeKind of
    tkInteger, tkChar, tkEnumeration, tkWChar: (
      MinValue: Longint;
      MaxValue: Longint;
      case TTypeKind of
        tkInteger, tkChar, tkWChar: ();
        tkEnumeration: (
          BaseType: PTypeInfo;
          NameList: ShortString));
        tkSet: (
          CompType: PPTypeInfo));

All ordinal types have a field OrdType of type TOrdType that says how many bytes are required and whether signed values are supported.

enum TOrdType { otSByte, otUByte, otSWord, otUWord, otSLong };

If the type is not a set, the minimum and maximum values are available in terms of ints.

If it is an enumerated type you can gain access to all the enumerated type value names. Sometimes, enumerated types are defined in terms of a subrange of values from another enumerated type. An example would be TBorderStyle which is defined in terms of two values from the TFormBorderStyle enumerated type. The BaseType field points to the RTTI table for the enumerated type used as a basis for this one.

If the type is a set, you can get to the RTTI table for the set's base type - the ordinal type whose values can be in a set of this type.

//C++Builder
struct
{
  TMetaClass *ClassType;
  PTypeInfo *ParentInfo;
  short PropCount;
  ShortStringBase UnitName;
};

//Delphi
tkClass: (
  ClassType: TClass;
  ParentInfo: PPTypeInfo;
  PropCount: Smallint;
  UnitName: ShortString
  {PropData: TPropData});

RTTI for a class type includes a class reference, a pointer to the ancestor class's RTTI table, the number of published properties and the name of the defining unit. Again, this name is only as long as it needs to be, hence the commented/missing field that follows. In addition, there is a TPropData record that follows containing data about all the published properties.

//C++Builder
struct TypInfo__1
{

} ;

struct TPropData
{
  Word PropCount;
  TypInfo__1 PropList;
} ;

//Delphi
TPropData = packed record
  PropCount: Word;
  PropList: record end;
  {PropList: array[1..PropCount] of TPropInfo}
end;

This time the commented out field at the end of the Pascal code can be accessed because an indeterminately long string does not precede it. However because this PropList field is supposed to be a variably sized array, its declaration is commented and substituted with an empty record. PropCount dictates how many TPropInfo records there are, and each TPropInfo record contains all the characteristics of a published property.

//C++Builder
struct TPropInfo
{
  PTypeInfo *PropType;
  void *GetProc;
  void *SetProc;
  void *StoredProc;
  int Index;
  int Default;
  short NameIndex;
  System::ShortString Name;
}; 

//Delphi
TPropInfo = packed record
  PropType: PTypeInfo;
  GetProc: Pointer;
  SetProc: Pointer;
  StoredProc: Pointer;
  Index: Integer;
  Default: Longint;
  NameIndex: Smallint;
  Name: ShortString;
end;

We have a pointer to the RTTI table for the property's type, a pointer to the property reader and property writer methods. Additionally there is a reference to the method, field or value that is used to determine whether to store the property value in the form file (specified with the stored directive), the property index (specified with the index directive), a default value (specified by the default directive, which is also used to dictate whether the property gets stored in the form file) and its name.

Accessing Non-Class RTTI, Part 2

The previous paragraph contains an important point. If you get a TPropInfo record that describes a published property, it contains a pointer to the TTypeInfo record that describes its type. So whilst Delphi programmers can happily use TypeInfo() to gain access to RTTI for most types, C++Builder users can use the PropType field of an appropriate TPropInfo, assuming a suitable published property can be found.

So going back to the previously mentioned example of getting RTTI (a valid PTypeInfo) for TFormBorderStyle, the type of a form's BorderStyle property, you can use any of these approaches.

Note: an instance of the class that publishes a property of the type in question need not exist - you can use the class itself.
PTypeInfo TypeInfo;
PPropInfo PropInfo = GetPropInfo((PTypeInfo)TForm::ClassInfo(), "BorderStyle");
if (PropInfo)
  TypeInfo = *(PropInfo->PropType);

or:

PTypeInfo TypeInfo;
PPropInfo PropInfo = GetPropInfo(__typeinfo(TForm), "BorderStyle");
if (PropInfo)
  TypeInfo = *(PropInfo->PropType);

Now back to the information in the TypInfo unit.

//C++Builder
struct
{
  TMethodKind MethodKind;
  Byte ParamCount;
  char ParamList[1024];
};

enum TMethodKind { mkProcedure, mkFunction, mkSafeProcedure, mkSafeFunction };
typedef Set<TParamFlag, pfVar, pfOut> TParamFlags;

//Delphi
tkMethod: (
  MethodKind: TMethodKind;
  ParamCount: Byte;
  ParamList: array[0..1023] of Char
  {ParamList: array[1..ParamCount] of
    record
      Flags: TParamFlags;
      ParamName: ShortString;
      TypeName: ShortString;
    end;
  ResultType: ShortString});

TMethodKind = (mkProcedure, mkFunction, mkSafeProcedure, mkSafeFunction);
TParamFlag = (pfVar, pfConst, pfArray, pfAddress, pfReference, pfOut);

Information about published methods is also quite full. We can find out if the method is a procedure or function, and how many parameters it has, whether each parameter has a var or const prefix, if it is an array, and also what its name and type name are.

Once you see the extent of the information recorded in RTTI (automatically for types used in __published component sections), you can begin to get an idea of how the Object Inspector does its job. It knows how to let you edit values, because it can find out all the details about the type. The Events page can manufacture event handlers because event properties have a tkMethod type identifier, and C++Builder can find out what sort of event handler to manufacture.

The TTypeData record has a lot of useful stuff in, but of course with all these non-standard strings and variably sized arrays dotted here and there, accessing the various constituent records could prove a bit tricky. Fortunately, there are a number of routines in the TypInfo unit to help us out, as explained below.

What Can We Do With RTTI?

The routines available in the TypInfo unit tend to take either a PTypeInfo or PPropInfo pointer - a pointer to an RTTI table or to a property information record as described above.

Remember that a PTypeInfo pointer can be obtained both from the Delphi TypeInfo() function for most types, from the ClassInfo() class method for class RTTI and from the PropType field of a TPropInfo record. It's interesting to note, that in order to make things simple, the TTypeData record for a class type also has ClassType field that points back to the class reference.

Accessing Type Description Information

We can gain easy access to the TTypeData record using GetTypeData() which returns a pointer to it:

PTypeData __fastcall GetTypeData(PTypeInfo TypeInfo);

If you want to find which unit a class is defined in, for example the main form, you could use the following code. It is more complicated than it could need to be because the UnitName field in the TTypeData record is declared as the unhelpful ShortStringBase type.

ShortString *S = (ShortString *)&(GetTypeData(PTypeInfo(Form1->ClassInfo()))->UnitName);
ShowMessage(*S);

To be more generic, bearing in mind the form's class name may have changed, we could use:

ShortString *S = (ShortString *)&(GetTypeData(PTypeInfo(Application->MainForm->ClassInfo()))->UnitName);
ShowMessage(*S);

Since all of the TypInfo routines that operate on properties want a PPropInfo, there are several ways of getting one:

typedef TPropInfo *TPropList[16380];
typedef TPropInfo *PPropInfo;
typedef PPropInfo *PPropList;

PPropInfo __fastcall GetPropInfo(PTypeInfo TypeInfo, const System::AnsiString PropName);
void __fastcall GetPropInfos(PTypeInfo TypeInfo, PPropList PropList);
int __fastcall GetPropList(PTypeInfo TypeInfo, TTypeKinds TypeKinds, PPropList PropList);

There are a small number of constants #defined in TypInfo.hpp for use when enquiring about properties. However, C++Builder 5 defines them badly making them unusable. Here are the erroneous definitions:

#define tkAny (System::Set<TTypeKind, tkUnknown, tkDynArray> () )
#define tkMethods (System::Set<TTypeKind, tkUnknown, tkDynArray> () )
#define tkProperties (System::Set<TTypeKind, tkUnknown, tkDynArray> () )
and here are some correct definitions, taken from C++Builder 4 and updated to avoid ambiguity problems:
#define tkAny (System::Set<Typinfo::TTypeKind, tkUnknown, tkDynArray> () << tkUnknown \
  << tkInteger << tkChar << tkEnumeration << tkFloat << tkString << tkSet << tkClass \
  << tkMethod << tkWChar << tkLString << tkWString << tkVariant << tkArray << tkRecord \
  << tkInterface << tkInt64 << tkDynArray )
#define tkMethods (System::Set<Typinfo::TTypeKind, tkUnknown, tkDynArray> () << tkMethod)
#define tkProperties (System::Set<Typinfo::TTypeKind, tkUnknown, tkDynArray> () << tkInteger \
  << tkChar << tkEnumeration << tkFloat << tkString << tkSet << tkClass << tkWChar << tkLString \
  << tkWString << tkVariant << tkArray << tkRecord << tkInterface << tkInt64 << tkDynArray )
You can correct your copy of TypInfo.hpp in order to use these symbols.

For example, to get the names of all the properties of a form listed in a listbox called ListBox1, you could write:

TPropList List;
int Count = GetPropList((PTypeInfo)ClassInfo(), tkAny, (PPropList)&List);
for (int i = 0; i < Count; i++)
  ListBox1->Items->Add(List[i]->Name);

or perhaps like the following which would only take as much memory as is required:

int Count = GetPropList((PTypeInfo)ClassInfo(), tkAny, NULL);
PPropList List = new PPropInfo[Count];
try
{
  GetPropList((PTypeInfo)ClassInfo(), tkAny, List);
  for (int i = 0; i < Count; i++)
    ListBox1->Items->Add(List[i]->Name);
}
__finally
{
  delete[] List;
}

Reading and writing arbitrary properties

There are a number of paired routines for reading/writing property values of a given name:

TypInfo routine

Properties to use them with

GetOrdProp(),SetOrdProp()

ordinal properties

GetEnumProp(),SetEnumProp()

enumerated type properties

GetSetProp(),SetSetProp()

set type properties

GetObjectProp(),SetObjectProp()

object properties

GetStrProp(), SetStrProp()

long or short string properties

GetWideStrProp(), SetWideStrProp()

wide string properties

GetFloatProp(), SetFloatProp()

floating point properties

GetVariantProp(), SetVariantProp()

variant properties

GetMethodProp(), SetMethodProp()

method properties (events)

GetInt64Prop(), SetInt64Prop()

64-bit Integer properties

GetInterfaceProp(), SetInterfaceProp()

Interface properties

The Get/SetOrdProp() routines both take an object reference and a pointer to a property information record and they either take or return an int. A four-byte int is big enough to represent any VCL-employed ordinal value and also any object reference and so you use a typecast to translate between your value of interest and the int.

int __fastcall GetOrdProp(System::TObject* Instance, PPropInfo PropInfo);
void __fastcall SetOrdProp(System::TObject* Instance, PPropInfo PropInfo, int Value);

The other routines are laid out in a similar way, but take or return a value of an appropriate type.

As of C++Builder 5 there are also overloaded versions of these routines, referred to as easy access routines, which do all the work of getting the type information for you. Here are the prototypes for the easy access ordinal type property get/set functions:

int __fastcall GetOrdProp(System::TObject* Instance, const AnsiString PropName);
void __fastcall SetOrdProp(System::TObject* Instance, const AnsiString PropName, int Value);
Note: you must be sure that a property exists with these easy access routines. If the property does not exist, you will typically get an Access Violation. You can avoid this by checking with the IsPublishedProp() function beforehand.

These property access routines are used by the VCL streaming mechanics to read and write form files. Once a property value is read from a form file, information is looked up (a TPropInfo record) and its type category is identified (TTypeKind). Then the relevant property writing routine is invoked to set the property value with the data read in from the form file.

Common properties with no common ancestor

You can often come up against a requirement to set a property in many components where there is no common ancestor that defines this property. Let's look at an example or two to emphasise the problem and see how RTTI can offer us a solution.

Example 1 - the Enabled property

Consider this scenario. You have a form with a number of controls on that all need to be disabled by having their Enabled property set to false. The controls might be a button, an edit control and a check box, but then again may be of some other types. You could implement a routine called DisableThem() as shown below that disables any controls.

void DisableThem(TControl * const *ControlsToChange, const int ControlsToChange_Size)
{
  for(int i = 0; i <= ControlsToChange_Size; i++)
    ControlsToChange[i]->Enabled = false;
}
...
DisableThem(OPENARRAY(TControl *, (Button1, Edit1, CheckBox1)));

Now consider that you also need to disable some menu items, which also have an Enabled property. The problem here is that the TMenuItem class adds Enabled as a new property of its own - TMenuItem is derived from TComponent that does not have an Enabled property. On the other hand, the TButton, TEdit and TCheckBox class all inherit the Enabled property from their common ancestor type TControl.

So the Enabled property of the TMenuItem and that of all the other classes are actually different properties - they just have the same names. This means we have to use a different coding approach.

void DisableThem(TComponent * const *ComponentsToChange, const int ComponentsToChange_Size)
{
  for(int i = 0; i <= ComponentsToChange_Size; i++)
  {
    if (dynamic_cast<TMenuItem *>(ComponentsToChange[i]))
      ((TMenuItem *)ComponentsToChange[i])->Enabled = false;
    else if (dynamic_cast<TControl *>(ComponentsToChange[i]))
      ((TControl *)ComponentsToChange[i])->Enabled = false;
  }
}
...
DisableThem(OPENARRAY(TComponent *, (Button1, Edit1, CheckBox1, Menu1)));

If we need to address other components with unique Enabled properties, this code will lengthen. A possibly better approach that takes advantage of the SetOrdProp() routine from the TypInfo unit follows.

#include <TypInfo.hpp>
...
void DisableThem(TComponent * const *ControlsToChange, const int ControlsToChange_Size)
{
  for (int i = 0; i <= ControlsToChange_Size; i++)
  {
    PPropInfo PropInfo = GetPropInfo(
      (PTypeInfo)ControlsToChange[i]->ClassInfo(), "Enabled");
    if (PropInfo)
      SetOrdProp(ControlsToChange[i], PropInfo, (int)false);
  }
}

This can be rewritten with the easy access routines like this:

#include <TypInfo.hpp>
...
void DisableThem(TComponent * const *ControlsToChange, const int ControlsToChange_Size)
{
  for (int i = 0; i <= ControlsToChange_Size; i++)
    if (IsPublishedProp(ControlsToChange[i], "Enabled"))
      SetOrdProp(ControlsToChange[i], "Enabled", (int)false);
}

The project group GenericPropertyExamples.Bpg that accompanies this paper shows this same idea in a couple of ways, one more generic than the other.

Example 2 - DataSource and DataField properties

The same sort of problem arises when trying to set up data-aware controls to refer to a particular datasource component. All data-aware controls have a DataSource property, and most of them have a DataField property. However each control adds these properties individually - none of them come from a common ancestor. We can take the same approach as above to set these properties en masse. This routine deals with the DataSource property.

#include <TypInfo.hpp>
...
void SetDataSource(TControl * const *ControlsToChange, const int ControlsToChange_Size, TDataSource *DS)
{
  for (int i = 0; i <= ControlsToChange_Size; i++)
  {
    PPropInfo PropInfo = GetPropInfo(
      (PtypeInfo)ControlsToChange[i]->ClassInfo(), "DataSource");
    if (PropInfo)
      SetOrdProp(ControlsToChange[i], PropInfo, (int)DS);
  }
}
...
SetDataSource(OPENARRAY(TControl *, (DBEdit1, DBText1, DBNavigator1)), DataSource1);

Using the easy access routines, this can be rewritten as:

#include <TypInfo.hpp>
...
void SetDataSource(TControl * const *ControlsToChange, const int ControlsToChange_Size, TDataSource *DS)
{
  for (int i = 0; i <= ControlsToChange_Size; i++)
  {
    if (IsPublishedProp(ControlsToChange[i], "DataSource"))
      SetOrdProp(ControlsToChange[i], "DataSource", (int)DS);
  }
}
...
SetDataSource(OPENARRAY(TControl *, (DBEdit1, DBText1, DBNavigator1)), DataSource1);

Converting between enumerated type values and strings

Enumerated types are programmatic devices to make programs easier to read and write by substituting recognisable terms for numeric values (which enumerated types are implemented as). It is normally the case that at compile time, all information about these textual terms is removed from the executable, but RTTI can change that.

Given a type:

enum TDayOfWeek { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday };

It is possible to start with a string "Tuesday" and get a TDayOfWeek value Tuesday out. It is also possible to do the reverse. The two routines GetEnumName() and GetEnumValue() do this and to work with them requires a typecast between any enumerated value and an Integer.

System::AnsiString __fastcall GetEnumName(PTypeInfo TypeInfo, int Value);
int __fastcall GetEnumValue(PTypeInfo TypeInfo, const System::AnsiString Name);

Given the enumerated type TFormBorderStyle, used for a form's BorderStyle property and defined as:

enum TFormBorderStyle { bsNone, bsSingle, bsSizeable, bsDialog, bsToolWindow, bsSizeToolWin };

then we can say:

#include <TypInfo.hpp>
...
//Get RTTI for the BorderStyle property
PPropInfo PropInfo = GetPropInfo((PTypeInfo)ClassInfo(), "BorderStyle");
//Could alternatively use this syntax
//PPropInfo PropInfo = GetPropInfo(__typeinfo(TForm), "BorderStyle");
if (PropInfo)
{
  PTypeInfo TypeInfo = *(PropInfo->PropType);
  //Store current BorderStyle
  String OldStyle = GetEnumName(TypeInfo, int(BorderStyle));
  //Now change BorderStyle to different value, specified as a string
  const String NewStyle = "bsToolWindow";
  BorderStyle = TFormBorderStyle(GetEnumValue(TypeInfo, NewStyle));
  //Write information about what happened on the caption bar
  Caption = Format("At design time, this form's border style was %s, now it is %s",
    ARRAYOFCONST((OldStyle, NewStyle)));
}

Alternatively, using easy access methods, we could rewrite it as:

#include <TypInfo.hpp>
...
if (IsPublishedProp(this, "BorderStyle"))
{
  //Store current BorderStyle
  String OldStyle = GetEnumProp(this, "BorderStyle");
  //Now change BorderStyle to different value, specified as a string
  const String NewStyle = "bsToolWindow";
  SetEnumProp(this, "BorderStyle", NewStyle);
  //Write information about what happened on the caption bar
  Caption = Format("At design time, this form's border style was %s, now it is %s",
    ARRAYOFCONST((OldStyle, NewStyle)));
}

You can find this code, and also an approach that uses a Pascal utility unit to get access to the relevant RTTI block in the project group EnumerationExamples.bpg that accompanies this paper.

Setting a component's default properties

When writing components you are able to specify default property values in the class declaration. This does not actually set the properties of any component instances to those defaults - that must still be done in the component constructor. Instead, it simply sets up one of the fields in the component's RTTI block. The C++Builder environment uses this information to reduce the volume of information that must go in the .DFM form file. Only properties whose values differ from any specified default go in the file.

As mentioned just above, it is still down to the developer to manually set the properties to their default values in the constructor. This can be a bit of a chore to do, particularly if there are several of them. To simplify the task we can use the RTTI information, looping through all the published properties that might have a default value and apply it. The code shown here (and available to test out in the PropEg.cpp component unit and RttiUnit.cpp utility unit) is based on code from Secrets Of Delphi 2 by Ray Lischner, published by The Waite Group.

...
__fastcall TDefaultProperty::TDefaultProperty(TComponent* Owner)
: TComponent(Owner)
{
  //This is a traditional way of setting
  //default property values - assign the
  //value required to each of the underlying
  //data fields one at a time, e.g.
  //FProperty1 = 10;
  //FProperty1 = 20;
  //FProperty1 = 30;

  //But here is a neater way as shown in "Secrets of Delphi 2"
  //use the RTTI to do it all automatically. The code
  //is then the same for each component constructor
  SetDefaults(this);
}

const NoDefault = 0x80000000;
#define tkPropsWithDefault (System::Set<TTypeKind, tkUnknown, tkInterface> () \
  << tkInteger << tkChar << tkEnumeration << tkSet )

void SetDefaults(TObject *Obj)
{
  //Find out how many properties we'll be considering
  int Count = GetPropList((PTypeInfo)(Obj->ClassInfo()), tkPropsWithDefault, NULL);
  //Allocate memory to hold their RTTI data
  PPropList List = new PPropInfo[Count];
  try
  {
    //Get hold of the property list in our new buffer
    GetPropList((PTypeInfo)(Obj->ClassInfo()), tkPropsWithDefault, List);
    //Loop through all the selected properties
    for (int i = 0; i < Count; i++)
      //If there is supposed to be a default value...
      if ((*List[i])->Default != NoDefault)
        //...then jolly well set it
        SetOrdProp(Obj, *List[i], (*List[i])->Default);
  }
  __finally
  {
    delete[] List;
  }
}

Copying properties from one component to another

Another possible use for RTTI is for helping to copy one object to another. C++Builder doesn't provide much in the way of support for copying VCL objects apart from the Assign() method, which must be coded specifically for each class in question. If you want to copy the values of common properties from one component to another then RTTI can help. Additionally, if you make up new classes, publishing properties that represent the important data will facilitate easy object copying if it ever becomes necessary.

The RttiUnit.cpp unit that accompanies this paper has a routine called CopyObject() defined in it. CopyObject() takes two object references - a source object and a target object. It iterates through all the published properties in the source object and attempts to set the value of the same property in the target. If the property doesn't exist in the target then it skips onto the next one.

Given a bitmap button, a normal button and an edit control on a form, you could use the following statements to ensure that all common properties of these objects are exact copies of each other using the following statements. Of course, given that the component co-ordinates are properties themselves, then the last two statements ensures that they don't all end up sitting on top of each other.

CopyObject(BitBtn1, Button1);
//Move button so you can see it
Button1->Left = BitBtn1->Left + BitBtn1->Width + 2;
CopyObject(BitBtn1, Edit1);
//Move edit so you can see it
Edit1->Left = Button1->Left + Button1->Width + 2;

The code is a bit too lengthy to list here but isn't really that far removed from the default property setting code above. Refer to the unit for the source. Also, the sample project CopyEg.bpr supplied with this paper shows an example of the function in use.

Summary

This paper has discussed the subject of Run-Time Type Information. After looking at why it exists we then looked at how we can access it and a number of applications of how it can be used. You can use it in many different ways to those suggested here, and hopefully you have been given a taster of how this remarkably under-used feature of C++Builder (and Delphi) can be employed.

About Brian Long

Brian Long used to work at Borland UK, performing a number of duties including Technical Support on all the programming tools. Since leaving in 1995, Brian has spent the intervening years as a trainer, trouble-shooter and mentor focusing on the use of the C#, Delphi and C++ languages, and of the Win32 and .NET platforms. In his spare time Brian actively researches and employs strategies for the convenient identification, isolation and removal of malware. If you need training in these areas or need solutions to problems you have with them, please get in touch or visit Brian's Web site.

Brian authored a Borland Pascal problem-solving book in 1994 and occasionally acts as a Technical Editor for Wiley (previously Sybex); he was the Technical Editor for Mastering Delphi 7 and Mastering Delphi 2005 and also contributed a chapter to Delphi for .NET Developer Guide. Brian is a regular columnist in The Delphi Magazine and has had numerous articles published in Developer's Review, Computing, Delphi Developer's Journal and EXE Magazine. He was nominated for the Spirit of Delphi award in 2000.