Team LiB
Previous Section Next Section

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.

 

Image 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_casts
 

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.

 

Image Note

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.

 

Image 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_casts
 

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?

 

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_casts 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 const2.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 which bp points.

 

Image 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.

 

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 AndQuery15.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

 
Image
 

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

 

Image Note

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.

 

 

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;

 

 
Team LiB
Previous Section Next Section