19.2. Run-Time Type Identification
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.
WARNING
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.
19.2.1. The dynamic_cast
Operator
A 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
.
Pointer-Type dynamic_cast
s
As 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.
INFO
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.
TIP
Best Practices
Performing a dynamic_cast
in a condition ensures that the cast and test of its result are done in a single expression.
Reference-Type dynamic_cast
s
A 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
}
}
19.2.2. The typeid
Operator
The 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?
INFO
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 type C&
:
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.
Using the typeid
Operator
Ordinarily, 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 whichbp
points.
WARNING
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.
19.2.3. Using RTTI
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.
INFO
Exercises Section 19.2.2
Exercise 19.6: Write an expression to dynamically cast a pointer to a Query_base
to a pointer to an AndQuery
(§ 15.9.1, p. 636). Test the cast by using objects of AndQuery
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 to AndQuery
. Repeat the test to ensure that your cast works correctly.
Exercise 19.8: Write a typeid
expression to see whether two Query_base
pointers point to the same type. Now check whether that type is an AndQuery
.
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.
The Class Hierarchy
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
};
A Type-Sensitive Equality Operator
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.
The Virtual equal
Functions
Each 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.
The Base-Class equal
Function
This 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.
19.2.4. The type_info
Class
The 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
Operation | Description |
---|---|
t1 == t2 | Returns true if the type_info objects t1 and t2 refer to the same type, false otherwise. |
t1 != t2 | Returns true if the type_info objects t1 and t2 refer to different types, false otherwise. |
t.name() | Returns a C-style character string that is a printable version of the type name. Type names are generated in a system-dependent way. |
t1.before(t2) | Returns a bool that indicates whether t1 comes before t2 . The ordering imposed by before is compiler dependent. |
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
INFO
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 exact type_info
support provided.
INFO
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;