RTTI has many vocal critics within the C++ community. They view RTTI as unnecessary, a potential source of program inefficiency, and a possible contributor to bad programming practices. Without delving into the debate over RTTI, let’s look at the sort of programming that you should avoid.
Consider the core of Listing 15.17:
Grand * pg;
Superb * ps;
for (int i = 0; i < 5; i++)
{
pg = GetOne();
pg->Speak();
if( ps = dynamic_cast<Superb *>(pg))
ps->Say();
}
By using typeid
and ignoring dynamic_cast
and virtual functions, you can rewrite this code as follows:
Grand * pg;
Superb * ps;
Magnificent * pm;
for (int i = 0; i < 5; i++)
{
pg = GetOne();
if (typeid(Magnificent) == typeid(*pg))
{
pm = (Magnificent *) pg;
pm->Speak();
pm->Say();
}
else if (typeid(Superb) == typeid(*pg))
{
ps = (Superb *) pg;
ps->Speak();
ps->Say();
}
else
pg->Speak();
}
Not only is this uglier and longer than the original, it has the serious flaw of naming each class explicitly. Suppose, for example, that you find it necessary to derive an Insufferable
class from the Magnificent
class. And suppose the new class redefines Speak()
and Say()
. With the version that uses typeid
to test explicitly for each type, you would have to modify the for
loop code, adding a new else if
section. The original version, however, requires no changes at all. The following statement works for all classes derived from Grand
:
pg->Speak();
And this statement works for all classes derived from Superb
:
if( ps = dynamic_cast<Superb *>(pg))
ps->Say();
If you find yourself using typeid
in an extended series of if else
statements, you should check whether you should instead use virtual functions and dynamic_cast
.
The C type cast operator, in Bjarne Stroustrup’s view, is too lax. For example, consider the following:
struct Data
{
double data[200];
};
struct Junk
{
int junk[100];
};
Data d = {2.5e33, 3.5e-19, 20.2e32};
char * pch = (char *) (&d); // type cast #1 – convert to string
char ch = char (&d); // type cast #2 - convert address to a char
Junk * pj = (Junk *) (&d); // type cast #3 - convert to Junk pointer
First, which of these three type casts makes any sense? Unless you resort to the implausible, none of them make much sense. Second, which of these three type casts are allowed? In C, all of them are. Stroustrup’s response to this laxity was to tighten up what is allowable for a general type cast and to add four type cast operators that provide more discipline for the casting process:
dynamic_cast
const_cast
static_cast
reinterpret_cast
Instead of using a general type cast, you can select an operator that is suited to a particular purpose. This documents the intended reason for the type cast and gives the compiler a chance to check that you did what you thought you did.
You’ve already seen the dynamic_cast
operator. To summarize, suppose High
and Low
are two classes, that ph
is type High *
, and that pl
is type Low *
. Then the following statement assigns a Low *
pointer to pl
only if Low
is an accessible base class (direct or indirect) to High
:
pl = dynamic_cast<Low *> ph;
Otherwise, the statement assigns the null pointer to pl
. In general, the operator has this syntax:
dynamic_cast < type-name > (expression)
The purpose of this operator is to allow upcasts within a class hierarchy (such type casts being safe because of the is-a relationship) and to disallow other casts.
The const_cast
operator is for making a type cast with the sole purpose of changing whether a value is const
or volatile
. It has the same syntax as the dynamic_cast
operator:
const_cast < type-name > (expression)
The result of making such a type cast is an error if any other aspect of the type is altered. That is, type_name
and expression
must be of the same type, except that they can differ in the presence or absence of const
or volatile
. Again, suppose High
and Low
are two classes:
High bar;
const High * pbar = &bar;
...
High * pb = const_cast<High *> (pbar); // valid
const Low * pl = const_cast<const Low *> (pbar); // invalid
The first type cast makes *pb
a pointer that can be used to alter the value of the bar
object; it removes the const
label. The second type cast is invalid because it attempts to change the type from const High *
to const Low *
.
The reason for this operator is that occasionally you may have a need for a value that is constant most of the time but that can be changed occasionally. In such a case, you can declare the value as const
and use const_cast
when you need to alter the value. This could be done using the general type cast, but the general type cast can also simultaneously change the type:
High bar;
const High * pbar = &bar;
...
High * pb = (High *) (pbar); // valid
Low * pl = (Low *) (pbar); // also valid
Because the simultaneous change of type and constantness may be an unintentional programming slip, using the const_cast
operator is safer.
The const_cast
is not all powerful. It can change the pointer access to a quantity, but the effect of attempting to change a quantity that is declared const
is undefined. Let’s clarify this statement with the short example shown in Listing 15.19.
// constcast.cpp -- using const_cast<>
#include <iostream>
using std::cout;
using std::endl;
void change(const int * pt, int n);
int main()
{
int pop1 = 38383;
const int pop2 = 2000;
cout << "pop1, pop2: " << pop1 << ", " << pop2 << endl;
change(&pop1, -103);
change(&pop2, -103);
cout << "pop1, pop2: " << pop1 << ", " << pop2 << endl;
return 0;
}
void change(const int * pt, int n)
{
int * pc;
pc = const_cast<int *>(pt);
*pc += n;
}
The const_cast
operator can remove the const
from const int * pt
, thus allowing the compiler to accept the following statement in change()
:
*pc += n;
However, because pop2
is declared as const
, the compiler may protect it from any change, as is shown by the following sample output:
pop1, pop2: 38383, 2000
pop1, pop2: 38280, 2000
As you can see, the calls to change()
alter pop1
but not pop2
. The pointer in change()
is declared as const int *
, so it can’t be used to change the value of the pointed-to int
. The pointer pc
has the const
cast away, so it can be used to change the pointed-to value, but only if that value wasn’t itself const
. Therefore, pc
can be used to alter pop1
but not pop2
.
The static_cast
operator has the same syntax as the other operators:
static_cast < type-name > (expression)
It’s valid only if type_name
can be converted implicitly to the same type that expression
has, or vice versa. Otherwise, the type cast is an error. Suppose that High
is a base class to Low
and that Pond
is an unrelated class. Then conversions from High
to Low
and Low
to High
are valid, but a conversion from Low
to Pond
is disallowed:
High bar;
Low blow;
...
High * pb = static_cast<High *> (&blow); // valid upcast
Low * pl = static_cast<Low *> (&bar); // valid downcast
Pond * pmer = static_cast<Pond *> (&blow); // invalid, Pond unrelated
The first conversion here is valid because an upcast can be done explicitly. The second conversion, from a base-class pointer to a derived-class pointer, can’t be done without an explicit type conversion. But because the type cast in the other direction can be made without a type cast, it’s valid to use static_cast
for a downcast.
Similarly, because an enumeration value can be converted to an integral type without a type cast, an integral type can be converted to an enumeration value with static_cast
. Also you can use static_cast
to convert double
to int
, to convert float
to long
, and to perform the various other numeric conversions.
The reinterpret_cast
operator is for inherently risky type casts. It doesn’t let you cast away const
, but it does allow other unsavory things. Sometimes a programmer has to do implementation-dependent, unsavory things, and using the reinterpret_cast
operator makes it simpler to keep track of such acts. It has the same syntax as the other three operators:
reinterpret_cast < type-name > (expression)
Here is a sample use:
struct dat {short a; short b;};
long value = 0xA224B118;
dat * pd = reinterpret_cast< dat *> (&value);
cout << hex << pd->a; // display first 2 bytes of value
Typically, such type casts would be used for low-level, implementation-dependent programming and would not be portable. For example, one system may store the bytes in a multibyte value in a different order than does a second system.
The reinterpret_cast
operator doesn’t allow just anything, however. For example, you can cast a pointer type to an integer type that’s large enough to hold the pointer representation, but you can’t cast a pointer to a smaller integer type or to a floating-point type. Another restriction is that you can’t cast a function pointer to a data pointer or vice versa.
The plain type cast in C++ is also restricted. Basically, it can do anything the other type casts can do, plus some combinations, such as a static_cast
or reinterpret_cast
followed by a const_cast
, but it can’t do anything else. Thus, the following type cast is allowed in C but, typically, not in C++ because for most C++ implementations the char
type is too small to hold a pointer implementation:
char ch = char (&d); // type cast #2 - convert address to a char
These restrictions make sense, but if you find such enforced good judgment oppressive, you still have C available.
Friends allow you to develop a more flexible interface for classes. A class can have other functions, other classes, and member functions of other classes as friends. In some cases, you may need to use forward declarations and to exert care in the ordering of class declarations and methods in order to get friends to mesh properly.
Nested classes are classes that are declared within other classes. Nested classes facilitate the design of helper classes that implement other classes but needn’t be part of a public interface.
The C++ exception mechanism provides a flexible way to deal with awkward programming events such as inappropriate values, failed I/O attempts, and the like. Throwing an exception terminates the function currently executing and transfers control to a matching catch
block. catch
blocks immediately follow a try
block, and for an exception to be caught, the function call that directly or indirectly led to the exception must be in the try
block. The program then executes the code in the catch
block. This code may attempt to fix the problem, or it can terminate the program. A class can be designed with nested exception classes that can be thrown when problems specific to the class are detected. A function can include an exception specification that identifies the exceptions that can be thrown in that function, although C++11 deprecates that feature. Uncaught exceptions (those with no matching catch
block) by default terminate a program. So do unexpected exceptions (those not matching an exception specification.)
The RTTI features allow a program to detect the type of an object. The dynamic_cast
operator is used to cast a derived-class pointer to a base-class pointer; its main purpose is to ensure that it’s okay to invoke a virtual function call. The typeid
operator returns a type_info
object. Two typeid
return values can be compared to determine whether an object is of a specific type, and the returned type_info
object can be used to obtain information about an object.
The dynamic_cast
, static_cast
, const_cast
, and reinterpret_cast
operators provide safer, better-documented type casts than the general type cast mechanism.
1. What’s wrong with the following attempts at establishing friends?
a.
class snap {
friend clasp;
...
};
class clasp { ... };
b.
class cuff {
public:
void snip(muff &) { ... }
...
};
class muff {
friend void cuff::snip(muff &);
...
};
c. class muff {
friend void cuff::snip(muff &);
...
};
class cuff {
public:
void snip(muff &) { ... }
...
};
2. You’ve seen how to create mutual class friends. Can you create a more restricted form of friendship in which only some members of Class B are friends to Class A and some members of A are friends to B? Explain.
3. What problems might the following nested class declaration have?
class Ribs
{
private:
class Sauce
{
int soy;
int sugar;
public:
Sauce(int s1, int s2) : soy(s1), sugar(s2) { }
};
...
};
4. How does throw
differ from return
?
5. Suppose you have a hierarchy of exception classes that are derived from a base exception class. In what order should you place catch
blocks?
6. Consider the Grand
, Superb
, and Magnificent
classes defined in this chapter. Suppose pg
is a type Grand *
pointer that is assigned the address of an object of one of these three classes and that ps
is a type Superb *
pointer. What is the difference in how the following two code samples behave?
if (ps = dynamic_cast<Superb *>(pg))
ps->say(); // sample #1
if (typeid(*pg) == typeid(Superb))
(Superb *) pg)->say(); // sample #2
7. How is the static_cast
operator different from the dynamic_cast
operator?
1. Modify the Tv
and Remote
classes as follows:
a. Make them mutual friends.
b. Add a state variable member to the Remote
class that describes whether the remote control is in normal or interactive mode.
c. Add a Remote
method that displays the mode.
d. Provide the Tv
class with a method for toggling the new Remote
member. This method should work only if the TV is in the on state.
Write a short program that tests these new features.
2. Modify Listing 15.11 so that the two exception types are classes derived from the logic_error
class provided by the <stdexcept>
header file. Have each what()
method report the function name and the nature of the problem. The exception objects need not hold the bad values; they should just support the what()
method.
3. This exercise is the same as Programming Exercise 2, except that the exceptions should be derived from a base class (itself derived from logic_error
) that stores the two argument values, the exceptions should have a method that reports these values as well as the function name, and a single catch
block that catches the base-class exemption should be used for both exceptions, with either exception causing the loop to terminate.
4. Listing 15.16 uses two catch
blocks after each try
block so that the nbad_index
exception leads to the label_val()
method being invoked. Modify the program so that it uses a single catch
block after each try
block and uses RTTI to handle invoking label_val()
only when appropriate.
18.119.235.79