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