Run-time type identification (RTTI) is provided through two operators:
• The
typeid
operator, which returns the type of a given expression
• The
dynamic_cast
operator, which safely converts a pointer or reference to a base type into a pointer or reference to a derived type
When applied to pointers or references to types that have virtual functions, these operators use the dynamic type (§ 15.2.3, p. 601) of the object to which the pointer or reference is bound.
These operators are useful when we have a derived operation that we want to perform through a pointer or reference to a base-class object and it is not possible to make that operation a virtual function. Ordinarily, we should use virtual functions if we can. When the operation is virtual, the compiler automatically selects the right function according to the dynamic type of the object.
However, it is not always possible to define a virtual. If we cannot use a virtual, we can use one of the RTTI operators. On the other hand, using these operators is more error-prone than using virtual member functions: The programmer must know to which type the object should be cast and must check that the cast was performed successfully.
RTTI should be used with caution. When possible, it is better to define a virtual function rather than to take over managing the types directly.
dynamic_cast
OperatorA dynamic_cast
has the following form:
dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)
where type must be a class type and (ordinarily) names a class that has virtual functions. In the first case, e
must be a valid pointer (§ 2.3.2, p. 52); in the second, e
must be an lvalue; and in the third, e
must not be an lvalue.
In all cases, the type of e
must be either a class type that is publicly derived from the target type, a public
base class of the target type, or the same as the target type. If e
has one of these types, then the cast will succeed. Otherwise, the cast fails. If a dynamic_cast
to a pointer type fails, the result is 0. If a dynamic_cast
to a reference type fails, the operator throws an exception of type bad_cast
.
dynamic_cast
sAs a simple example, assume that Base
is a class with at least one virtual function and that class Derived
is publicly derived from Base
. If we have a pointer to Base
named bp
, we can cast it, at run time, to a pointer to Derived
as follows:
if (Derived *dp = dynamic_cast<Derived*>(bp))
{
// use the Derived object to which dp points
} else { // bp points at a Base object
// use the Base object to which bp points
}
If bp
points to a Derived
object, then the cast will initialize dp
to point to the Derived
object to which bp
points. In this case, it is safe for the code inside the if
to use Derived
operations. Otherwise, the result of the cast is 0. If dp
is 0, the condition in the if
fails. In this case, the else
clause does processing appropriate to Base
instead.
We can do a
dynamic_cast
on a null pointer; the result is a null pointer of the requested type.
It is worth noting that we defined dp
inside the condition. By defining the variable in a condition, we do the cast and corresponding check as a single operation. Moreover, the pointer dp
is not accessible outside the if
. If the cast fails, then the unbound pointer is not available for use in subsequent code where we might forget to check whether the cast succeeded.
Performing a
dynamic_cast
in a condition ensures that the cast and test of its result are done in a single expression.
dynamic_cast
sA dynamic_cast
to a reference type differs from a dynamic_cast
to a pointer type in how it signals that an error occurred. Because there is no such thing as a null reference, it is not possible to use the same error-reporting strategy for references that is used for pointers. When a cast to a reference type fails, the cast throws a std::bad_cast
exception, which is defined in the typeinfo
library header.
We can rewrite the previous example to use references as follows:
void f(const Base &b)
{
try {
const Derived &d = dynamic_cast<const Derived&>(b);
// use the Derived object to which b referred
} catch (bad_cast) {
// handle the fact that the cast failed
}
}
typeid
OperatorThe second operator provided for RTTI is the typeid
operator. The typeid
operator allows a program to ask of an expression: What type is your object?
Exercises Section 19.2.1
Exercise 19.3: Given the following class hierarchy in which each class defines a
public
default constructor and virtual destructor:class A { /* . . . */ };
class B : public A { /* . . . */ };
class C : public B { /* . . . */ };
class D : public B, public A { /* . . . */ };which, if any, of the following
dynamic_cast
s fail?(a) A *pa = new C;
B *pb = dynamic_cast< B* >(pa);
(b) B *pb = new B;
C *pc = dynamic_cast< C* >(pb);
(c) A *pa = new D;
B *pb = dynamic_cast< B* >(pa);Exercise 19.4: Using the classes defined in the first exercise, rewrite the following code to convert the expression
*pa
to the typeC&
:if (C *pc = dynamic_cast< C* >(pa))
// use C's members
} else {
// use A's members
}Exercise 19.5: When should you use a
dynamic_cast
instead of a virtual function?
A typeid
expression has the form typeid(e)
where e
is any expression or a type name. The result of a typeid
operation is a reference to a const
object of a library type named type_info
, or a type publicly derived from type_info
. § 19.2.4 (p. 831) covers this type in more detail. The type_info
class is defined in the typeinfo
header.
The typeid
operator can be used with expressions of any type. As usual, top-level const
(§ 2.4.3, p. 63) is ignored, and if the expression is a reference, typeid
returns the type to which the reference refers. When applied to an array or function, however, the standard conversion to pointer (§ 4.11.2, p. 161) is not done. That is, if we take typeid(a)
and a
is an array, the result describes an array type, not a pointer type.
When the operand is not of class type or is a class without virtual functions, then the typeid
operator indicates the static type of the operand. When the operand is an lvalue of a class type that defines at least one virtual function, then the type is evaluated at run time.
typeid
OperatorOrdinarily, we use typeid
to compare the types of two expressions or to compare the type of an expression to a specified type:
Derived *dp = new Derived;
Base *bp = dp; // both pointers point to a Derived object
// compare the type of two objects at run time
if (typeid(*bp) == typeid(*dp)) {
// bp and dp point to objects of the same type
}
// test whether the run-time type is a specific type
if (typeid(*bp) == typeid(Derived)) {
// bp actually points to a Derived
}
In the first if
, we compare the dynamic types of the objects to which bp
and dp
point. If both point to the same type, then the condition succeeds. Similarly, the second if
succeeds if bp
currently points to a Derived
object.
Note that the operands to the typeid
are objects—we used *bp
, not bp
:
// test always fails: the type of bp is pointer to Base
if (typeid(bp) == typeid(Derived)) {
// code never executed
}
This condition compares the type Base*
to type Derived
. Although the pointer points at an object of class type that has virtual functions, the pointer itself is not a class-type object. The type Base*
can be, and is, evaluated at compile time. That type is unequal to Derived
, so the condition will always fail regardless of the type of the object to which
bp
points.
The
typeid
of a pointer (as opposed to the object to which the pointer points) returns the static, compile-time type of the pointer.
Whether typeid
requires a run-time check determines whether the expression is evaluated. The compiler evaluates the expression only if the type has virtual functions. If the type has no virtuals, then typeid
returns the static type of the expression; the compiler knows the static type without evaluating the expression.
If the dynamic type of the expression might differ from the static type, then the expression must be evaluated (at run time) to determine the resulting type. The distinction matters when we evaluate typeid(*p)
. If p
is a pointer to a type that does not have virtual functions, then p
does not need to be a valid pointer. Otherwise, *p
is evaluated at run time, in which case p
must be a valid pointer. If p
is a null pointer, then typeid(*p)
throws a bad_typeid
exception.
As an example of when RTTI might be useful, consider a class hierarchy for which we’d like to implement the equality operator (§ 14.3.1, p. 561). Two objects are equal if they have the same type and same value for a given set of their data members. Each derived type may add its own data, which we will want to include when we test for equality.
Exercises Section 19.2.2
Exercise 19.6: Write an expression to dynamically cast a pointer to a
Query_base
to a pointer to anAndQuery
(§ 15.9.1, p. 636). Test the cast by using objects ofAndQuery
and of another query type. Print a statement indicating whether the cast works and be sure that the output matches your expectations.Exercise 19.7: Write the same cast, but cast a
Query_base
object to a reference toAndQuery
. Repeat the test to ensure that your cast works correctly.Exercise 19.8: Write a
typeid
expression to see whether twoQuery_base
pointers point to the same type. Now check whether that type is anAndQuery
.
We might think we could solve this problem by defining a set of virtual functions that would perform the equality test at each level in the hierarchy. Given those virtuals, we would define a single equality operator that operates on references to the base type. That operator could delegate its work to a virtual equal
operation that would do the real work.
Unfortunately, this strategy doesn’t quite work. Virtual functions must have the same parameter type(s) in both the base and derived classes (§ 15.3, p. 605). If we wanted to define a virtual equal
function, that function must have a parameter that is a reference to the base class. If the parameter is a reference to base, the equal
function could use only members from the base class. equal
would have no way to compare members that are in the derived class but not in the base.
We can write our equality operation by realizing that the equality operator ought to return false
if we attempt to compare objects of differing type. For example, if we try to compare a object of the base-class type with an object of a derived type, the ==
operator should return false
.
Given this observation, we can now see that we can use RTTI to solve our problem. We’ll define an equality operator whose parameters are references to the base-class type. The equality operator will use typeid
to verify that the operands have the same type. If the operands differ, the ==
will return false
. Otherwise, it will call a virtual equal
function. Each class will define equal
to compare the data elements of its own type. These operators will take a Base&
parameter but will cast the operand to its own type before doing the comparison.
To make the concept a bit more concrete, we’ll define the following classes:
class Base {
friend bool operator==(const Base&, const Base&);
public:
// interface members for Base
protected:
virtual bool equal(const Base&) const;
// data and other implementation members of Base
};
class Derived: public Base {
public:
// other interface members for Derived
protected:
bool equal(const Base&) const;
// data and other implementation members of Derived
};
Next let’s look at how we might define the overall equality operator:
bool operator==(const Base &lhs, const Base &rhs)
{
// returns false if typeids are different; otherwise makes a virtual call to equal
return typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}
This operator returns false
if the operands are different types. If they are the same type, then it delegates the real work of comparing the operands to the (virtual) equal
function. If the operands are Base
objects, then Base::equal
will be called. If they are Derived
objects, Derived::equal
is called.
equal
FunctionsEach class in the hierarchy must define its own version of equal
. All of the functions in the derived classes will start the same way: They’ll cast their argument to the type of the class itself:
bool Derived::equal(const Base &rhs) const
{
// we know the types are equal, so the cast won't throw
auto r = dynamic_cast<const Derived&>(rhs);
// do the work to compare two Derived objects and return the result
}
The cast should always succeed—after all, the function is called from the equality operator only after testing that the two operands are the same type. However, the cast is necessary so that the function can access the derived members of the right-hand operand.
equal
FunctionThis operation is a bit simpler than the others:
bool Base::equal(const Base &rhs) const
{
// do whatever is required to compare to Base objects
}
There is no need to cast the parameter before using it. Both *this
and the parameter are Base
objects, so all the operations available for this object are also defined for the parameter type.
type_info
ClassThe exact definition of the type_info
class varies by compiler. However, the standard guarantees that the class will be defined in the typeinfo
header and that the class will provide at least the operations listed in Table 19.1.
Table 19.1. Operations on type_info
The class also provides a public
virtual destructor, because it is intended to serve as a base class. When a compiler wants to provide additional type information, it normally does so in a class derived from type_info
.
There is no type_info
default constructor, and the copy and move constructors and the assignment operators are all defined as deleted (§ 13.1.6, p. 507). Therefore, we cannot define, copy, or assign objects of type type_info
. The only way to create a type_info
object is through the typeid
operator.
The name
member function returns a C-style character string for the name of the type represented by the type_info
object. The value used for a given type depends on the compiler and in particular is not required to match the type names as used in a program. The only guarantee we have about the return from name
is that it returns a unique string for each type. For example:
int arr[10];
Derived d;
Base *p = &d;
cout << typeid(42).name() << ", "
<< typeid(arr).name() << ", "
<< typeid(Sales_data).name() << ", "
<< typeid(std::string).name() << ", "
<< typeid(p).name() << ", "
<< typeid(*p).name() << endl;
This program, when executed on our machine, generates the following output:
i, A10_i, 10Sales_data, Ss, P4Base, 7Derived
The
type_info
class varies by compiler. Some compilers provide additional member functions that provide additional information about types used in a program. You should consult the reference manual for your compiler to understand the exacttype_info
support provided.
Exercises Section 19.2.4
Exercise 19.9: Write a program similar to the last one in this section to print the names your compiler uses for common type names. If your compiler gives output similar to ours, write a function that will translate those strings to more human-friendly form.
Exercise 19.10: Given the following class hierarchy in which each class defines a
public
default constructor and virtual destructor, which type name do the following statements print?class A { /* . . . */ };
class B : public A { /* . . . */ };
class C : public B { /* . . . */ };
(a) A *pa = new C;
cout << typeid(pa).name() << endl;
(b) C cobj;
A& ra = cobj;
cout << typeid(&ra).name() << endl;
(c) B *px = new B;
A& ra = *px;
cout << typeid(ra).name() << endl;