25 RTTI and casting operators
Bhushan Trivedi
Introduction
In this module, we will extend our discussion on RTTI and will discuss how one critical casting operator called dynamic_cast can be deployed to provide a more flexible runtime assessment of the type and take a decision based on whether the type that we are checking belongs to a given hierarchy of objects or not. We will look at three more casting operations known as a static cast which is used for normal casting operations like casting an int value into a float value and reinterpret cast which is used for abnormal casting, for example, casting an int value into a pointer value. We will then see how typeid and dynamic_cast can be applied for templatized objects, for cross-casting and downcasting.
The shortcoming of typeid
We have hinted about shortcomings of typeid while presenting a solution to our News problem using typeid. The typeid was used to check if the news is of type Text and if so, we enabled the printing option. Now consider a case that we have a new type of news called hText (HTML type text), which is inherited from Text. Another class which is inherited from Text involves files generated from editors like the word, rtf_Text class which contains RTF(rich text format) files. You can see that both of them are different to each other, hText files contain HTML tags while the rtf_Text files contain typical rich text commands for boldface etc. However, both of them are text and should be allowed to be printed as per user’s instruction. To handle all these cases, we need to redesign our typeid checking code as follows.
if (typeid(rNews) == typeid(Text) ||
typeid(rNews) == typeid(hText) ||
typeid(rNews) == typeid(rtf_Text))
{ /* enable print */}
Above solution is simple but not very good. This solution works for a while but when a new class is added, for example, XML and OpenXML types may be added after a while, the programmer needs to modify this code, add two more checks to the above-mentioned lines and so on. When one of the types goes out of favor of the user, we may not remove that part and the program continues to run but it wastes precious time checking for nonexistent types A better solution is to remove those types from checking. All in all, any change in the hierarchy demands the programmers to change the code. We need to have a more flexible solution where even when the hierarchy changes the code does not change. Unfortunately, typeid cannot provide us that. The answer is to use dynamic_cast. Let us introduce dynamic_cast first and then see how it can solve our problem.
Dynamic_cast
The C style casting seems omnipresent and used by most programmers. However, the ANSI C++ has provided four additional types of casts and C style casting is not recommended for casting any longer. Out of four casting operators introduced by ANSI C++, the dynamic cast is specifically designed for dealing with polymorphic objects. Let us discuss a few important things about the dynamic cast. We will learn about other operators soon in this module itself.
- Dynamic cast casts a pointer to one type or reference into pointer or reference to another type only if the pointer to be cast to belongs to right hierarchy and points to the right object for the source pointer to be acceptable.
- Syntax of dynamic cast is Destination_Pointer/Reference = dynamic_cast <destination object pointer or reference type> (source object pointer or reference). Please note the first set of braces are angled braces or <> while the second set of braces are normal braces or ().
- Both source and dynamic objects must be polymorphic objects
- Pointer to derived cast can always be cast to base class pointer using dynamic_cast
- Pointer to base class object can only be cast to a derived class pointer, if the base class pointer is actually pointing to the derived class object, using dynamic cast and not otherwise
- Because of properties mentioned in 4 and 5, dynamic_cast is called a SAFE casting method. That is safe because only when the pointers are pointing to right objects the casting process is successful and not otherwise
- The casting of pointers or references is only possible to be done using dynamic_cast. The objects themselves cannot be cast.
- If pointers are cast and the dynamic_cast fails, it returns null pointer so the programmer must check for the pointer being returned is null to see if the dynamic cast is successful or otherwise.
- If references are cast and the dynamic cast fails, a bad_cast exception is thrown. Thus the call to dynamic_cast must be kept inside the try block and an appropriate exception handler must be written.
//PolymorphicDynamic_cast.cpp /
/Program 27.1 block 1
#include <iostream> #include <typeinfo>
#include <string>
using namespace std;
#include <iostream>
class Shape
{
int l_Style;
int fill;
public:
virtual void draw() { cout << “It is Shape \n”; }
};
class Circle:public Shape
{
int R, X, Y;
public:
Circle (int t_R, int t_X, int t_Y): R(t_R),X(t_X),Y(t_Y) {};
void draw() { cout << “It is Circle \n”; }
};
class Square: public Shape
{
int Len, X, Y;
public:
Square (int t_Len, int t_X, int t_Y):Len(t_Len),X(t_X),Y(t_Y) {}
void draw() { cout << “It is Square \n”; }
};
int main()
{
Shape t_Shape, *pShape;
Circle t_Circle(3,3,3), *pCircle;
Square t_Square(5,4,3), *pSquare;
pShape = dynamic_cast<Shape *> (&t_Shape);
if (pShape)
cout << “Shape to Shape pointer \n”;
else
cout << “No Shape to Shape pointer \n”;
pCircle = dynamic_cast<Circle *> (&t_Circle);
if (pCircle)
cout << “Circle to circle pointer \n”;
else
cout << “No Circle to circle pointer \n”;
pSquare = dynamic_cast<Square *> (&t_Shape);
if (pSquare)
cout << “Shape to Square pointer \n”;
else
cout << “No Shape to square pointer \n”;
pShape = dynamic_cast<Shape *> (&t_Circle);
if (pShape)
cout << “Circle to Shape pointer\n”;
else
cout << “No Circle to Shape pointer \n”;
pShape = &t_Square;
cout << “pShape points t_Square \n”;
pSquare = dynamic_cast<Square *> (pShape);
if (pSquare) cout << “pShape to pSquare\n”;
else cout << “No pShape to pSquare \n”;
pShape = &t_Shape;
cout << “pShape points t_Shape \n”;
if (pCircle) cout << “pShape to pSquare”;
else cout << “No pShape to pSquare\n”;
pCircle = & t_Circle;
cout << “pCircle points t_Circle \n”;
pShape = dynamic_cast<Shape *> (pCircle);
if (pShape) cout << “pCircle to pShape\n”;
else cout << “No pCircle to pShape \n”;
}
Shape to Shape pointer
Circle to circle pointer
No Shape to square pointer
Circle to Shape pointer
pShape points t_Square
pShape to pSquare
pShape points t_Shape
No pShape to pSquare
pCircle points t_Circle
pCircle to pShape
Program ended with exit code: 0
The program contains the same set of objects we have already introduced in earlier programs, the most interesting part of the entire content of 27.1 is the output which clearly indicates what is successful while dynamic casting and what is not. You can see that circle pointer can be cast to shape pointer but not vice versa. When pShape is pointing to a Shape object, the dynamic cast is unsuccessful while casting it to a circle pointer. On the contrary, when the pShape is pointing to a circle object, it is successful. Earlier two categories applying the dynamic cast to cast base class pointer to a base class and derived class pointer to a derived class which obviously are successful operations. Once we have understood how the dynamic cast functions, it is the time to apply it to replace the typeid operator in the News problem.
Using dynamic cast in News
There are some cases in which the typeid can be replaced by a dynamic cast and the code becomes much more flexible and easy. The News hierarchy problem is just one such case. Code using typeid is now converted to a much simpler code as shown below.
case PRINT:
try
{ Text & rText = dynamic_cast<Text&> (r_News); cout << “printing is enabled \n”;
rText.Print();
}
catch (bad_cast)
{
cout << “Printing is not enabled \n”;
// printing is disabled
};
break;
What did we do? We have replaced a series of typeid checks and a C style casting by a single dynamic cast. R We do not need to individually match the typeid of r_News to be either Text or each of its derived classes one after another now. A single statement using dynamic cast serves for all such class comparison into a single operation. We just try to dynamic cast r_News into Text. Only if the r_News refers to a news which is either of type Text or any derived class, the dynamic_cast is successful and the printing is enabled. Otherwise, the control transfers to the catch when it does not enable printing. As simple as that! Isn’t it!
What if some other classes are added or removed from the hierarchy? As long as the News refers to any object belonging to the hierarchy, there is no issue, it will safely cast the r_News to Text and printing can take place. The beauty of this solution is that we do not need to specify anything related to the class or hierarchy unlike the earlier solution based on typeid and when the hierarchy or the classes themselves changes, the code does not change. This is probably the biggest advantage of the flexibility of dynamic_cast. The class hierarchy is now independent of the code and has absolutely no effect on the code if there is any change in it! Once the programmer has coded an addition to the hierarchy, the newly added class is readily available for testing by our dynamic cast. The designer never provides their code to the user programmer to preserve their individual property rights and such a design enables them to do so! What will be the output if following is our main program?
hText t_hText
rClick(t_hText, PRINT);
nonText t_nonText;
rClick(t_nonText,PRINT);
Our dynamic cast tries to cast a reference to type News of type hText in case 1 while from nonText to Text in case 2. In case 1, as hText is part of the hierarchy derived from Text, it is successful while in the other case, it is not, so the cast fails.
After looking at the dynamic cast, let us look at other types of casting operators. Let us being with const_cast, which casts a const variable to non-const.
Const_cast
Const_cast is special as it can do something other casting operations cannot do. It converts a const into a non-const. However, the const variable under consideration is contextual const, and not by definition, for example, const references which are commonly passed to a function, which actually is non-const values. Let us take an example to understand Look at the output of the program 27.2 to check. Const int pointer passed to this function is converted back to a normal pointer using a const cast and the int value is modified. Thefunction returns the modified int. The function is rightly called dangerous as it does something other than the prototype suggests. The prototype suggests that const int reference is passed to it. That means if the int value changes during the process of executing that function, it won’t be compiled or return an error that we are trying to modify a const value or at least the changed value is not reflected back!. However, nothing of that sort happens. The integer i_2 is passed to the function as a constant reference and to our horror, it is changed from 10 to 11. Const cast works on pointers as well as references. It does not work on objects. The const values that it converts to non-const are only contextual const. The normal constants cannot be converted to non-const. for example, the i_1 value which is designed as const int does not change after calling the dangerF() unlike i_2.
You can see that i_1 being a real const value, does not change, but the i_2, which is not a real const, is changed to nonconst by the const cast. We have passed a cost int reference to dangerF but we could easily have passed const int pointer and the result would be the same. The reference is used because it is always considered to be a good programming when one passes a const reference in place of a value parameter. You now have at least one argument against that practice.
//Program 26.2
//const-cast.cpp
#include <iostream>
#include <string>
using namespace std;
void DangerF(const int & t_i_1)
{
int & t_i_2 = const_cast <int &> (t_i_1);
t_i_2++;
}
int main()
{
const int i_1=10;
int i_2 = 10;
cout << “Value of i1 is ” << i_1 << ” i2 is ” << i_2<< “\n”;
DangerF(i_1);
DangerF(i_2);
cout << “Value of i1 is ” << i_1 << ” i2 is ” << i_2<< “\n”;
}
Value of i1 is 10 i2 is 10
Value of i1 is 10 i2 is 11
Program ended with exit code: 0
Static_cast and reinterpret_cast
The static cast is designed to replace normal c style casting for normal operations like casting an int to float etc. Reinterpret cast is designed to replace normal c style casting for abnormal operations like casting an integer to a pointer value. Following are examples.
//Static_cast.cpp
#include <iostream>
using namespace std;
int main()
{
int i_1 = 10;
float f_2 = static_cast<float>(i_1);
- // int *p_i = static_cast<int *>(i_1); return 0;
}
// cpp #include <iostream> using namespace std;
int main()
{
int i_1 = 10;
//reinterpret_cast< float>(i_1);
int *p_i = reinterpret_cast<int *>(i_1);
return 0;
}
There are two casting operations to be performed. One is from int to float while another is from int to int*. The first type of casting operation is a normal operation which a programmer needs to do often. The second type of operations is normally done by mistake. The C style casting does not differentiate between both types of operations and thus the C++ standardized committee has decided to provide two different casting operators, static_cast is to be used for normal casting operations while reinterpret cast is to be used for abnormal casts like converting int to int*. However, if you use them for other operations like shown above, the program cannot compile unless you comment those lines. Point is, the programmer must know exactly what they wanted to do and use a specific type of operator. For normal operations, he chooses static_cast. If he makes mistake and does something wrong, the static cast won’t compile and he learns about the mistake there and then. On the other way round, if he really wants to convert an int value into a pointer, he will have to use reinterpret cast. He is successful only if he is really doing so, and not otherwise. Thus, even when the programmer wants to do an abnormal casting, he will have to be careful about the syntax. Remaining alert about syntax actually saves a lot of precious time when the c style casting is done inappropriately and the problem props up at a later stage in some other function of the program and the programmer has to spend a substantial amount to time to find out the exact reason for the error.
RTTI with templates
We looked at templates in module 16 and 17. The advantage of templates is that they allow us to write a type-less code. The generic functions that we write can work with any type of arguments, while the generic class that we write can work on any data types. We also have seen that when wanting a generic function to be used for a specific type, we provide that type as an argument in angular brackets like <int> or <employee> etc. and the typical generic function or class is instantiated for that type at that point in time. One can use that class as a normal class thereafter.
When we use templates and write the code using the generic type, sometimes we need to write a type specific part, especially when we are dealing with validations based on type. For example, we may check if the type is a pointer than it is not null. We may be using an employee as an object to be used in a class and we would like to check if the integer data is employee number than it is not negative. If it is string than they are employee names, which also are part of employee names in the database and so on. The idea is to provide a routine which is dealing with both string and int using a single generic type to provide that check.
This problem of dealing different types differently can be solved in multiple ways as we discussed in modules 16 and 17. One may also go for a specialization and have different functions and classes for different types of values and even can have overloaded functions, each for a different type. However, when most of the code is similar and only a type related few validations are to be done, typeid comes to the rescue. In many real world cases, people use templates and also virtual functions in a single program. Using RTTI alongside does not offer much additional overhead and thus a designer might choose this approach. The advantage is the rest of the code remains the same and periodic maintenance becomes easier. Let us see how we can provide a check for a generic type using an example depicted in 27.3. We can use typeid to check the type of the variable at runtime. Let us take a typical example to see how typeid is used in a typical situation. We have a contrived and simple example where the user uses an input of a specific type. Unless that type is the same as the search array type, we will display a typical message. The code contains two different types, Type, and u_Type. We want both the types to be the same. Unless it is so, there is no point in calling the search routine. The search routine checks for that only. For simplicity, we have omitted the search part itself.
//Program 26.3
//TemplateTypeid.cpp
#include <iostream>
#include <string>
using namespace std;
template <typename Type, typename u_Type=int>
class t_Search
{
public:
Type Search(Type Array[])
{
u_Type t_Input;
cout << “Please Input the value “;
cin >> t_Input;
cout << “\n”;
if (typeid(u_Type)!=typeid(Type))
{
cout << “Please enter data of type “<< typeid(Type).name()<< “\n”;
}
// search routine, we omit that at the moment return 0;
}
};
int main()
{
char Array[]= “This is testing” ;
t_Search<char> t_Class;
int result = t_Class.Search(Array);
}
Please Input the value 2
Please enter data of type char
Program ended with exit code: 0
You might have a query that we do not have the class t_Search with two arguments of two different types. Rather, the program should be written with only one Type, now one can provide only a single type and the problem is solved! However, the real problem may be much more complex. We might need to match type and a subtype derived from it or these two types are used for two different purposes in the same routine but only when a search is to be called, these two types must match etc. The real problem might demand two different types of data and can work with different types of data for the most part of the program but not the search algorithm part.
However, we do not right now bother much about the intension behind using two different types. The most critical part of the code, that we want to concentrate, is that we want two types which are generic while the code is written to match with another type at runtime to allow or disallow something. Our simple code can actually demonstrate the same. One can use typeid in another fashion for providing a specific type of validation as mentioned before. Let us try to see how that can be done.
We have two different types of arrays in 27.4. We want the search to happen on both arrays but would like to use a single routine for the same. We also want specific validations to be performed before we move further. The code described in 27.4 showcases how that can be done using typeid.
//Program 26.4
//TemplateType.cpp
#include <iostream>
#include <string>
using namespace std;
template <typename Type>
Type Search(Type Array[])
{
if (typeid(Type)== typeid(int)) {
// provide integer validation cout << “int validation done \n”;
}
if (typeid(Type) == typeid(char)) {
// provide char validation
cout << “char validation done \n”;
}
if (typeid(Type) == typeid(char *)) {
// provide string validations
cout << “char* validation done \n”;
}
// rest of the code return 0;
}
int main() {
char cArray[]= “A character Array” ;
- // following does character validation int result1 = Search(cArray);
int iArray[]={1,2,3,4,5,6,7,8,9,0}; int result2 = Search(iArray);
- // above code does integer validation
}
Cross-casting and downcasting are made safe Consider a case of a class called Indian Cricketer which is derived from two classes multiply, Indian and Cricketer. class IndianCricketer: public Indian, public Cricketer; Now we have a pointer pIndian defined to point to Indian.
Indian * pIndian;
As per law, we can make it point to a derived class of it, so the following statement is valid. pIndian = new IndianCricketer(Mithali); Now we take another pointer pCricketer of type pointer to a Cricketer object.
Cricketer * pCricketer;
Now, what if we write the following statement? We want the pCricketer pointer to be assigned whatever value pIndian contains.
pCricketer = pIndian;
logically, the pIndian pointer is an IndianCricketer object where pCricketer, being a base class pointer, can point to. So in above case, the compiler intervenes and allows this assignment. However, this may not be a good thing to do if pIndian is not pointing to an object of IndianCricketer. Assignments of above type are not considered to be good programming practice and not encouraged before the dynamic cast was introduced.
That means, if we want to cast one of this pointer into another, that won’t happen naturally in such cases. We better use dynamic_cast for doing so. The dynamic_cast, like in the previous case, is only successful if the types are compatible, in above case, it only casts the pIndian pointer to pCricketer pointer if pIndian points to IndianCricketer and not otherwise.
We may need to write
pCricketer = dynamic_cast<Cricketer *> (pIndian);
Above process is known as cross-casting. When there are two base classes, B1 and B2 derived multiply into D, casting a pointer to B1 to pointer to B2 or vice versa is called cross-casting. That was not considered to be a good programming practice in past but when cross-casting is done with the help of dynamic casting, it is considered good. If we want to have another pointer to IndianCricketer object as follows IndianCricketer *pIC; We can also cast the pIndian pointer successfully to pIC if the pIndian is pointing to IndianCricketer and not otherwise using the following statement.
pIC= dynamic_cast<IndianCricketer *> (pIndian)
We have already seen that process is known as Downcasting. The power of dynamic_cast is that two risky operations, cross-casting and Downcasting can be carried out in a safe manner.
Summary
In this module, we have looked at four casting operators provided by C++. We have seen how typeid based solutions cannot work satisfactorily when we need to compare with multiple types belongs to same hierarchy. The problem is solved using dynamic_cast. We have seen some examples to illustrate how static cast, reinterpret and const casts are working. We have seen how template based code can use typeid to check the actual type at runtime and provide type based validations. Finally, we have seen how cross-casting and downcasting are made safe using dynamic_cast.