Discussion
Team LiB
Previous Section Next Section

Discussion

One problem with C-style casts is that they provide one syntax to do subtly different things, depending on such vagaries as the files that you #include. The C++ casts, while retaining some of the dangers inherent in casts, avoid such ambiguities, clearly document their intent, are easy to search for, take longer to write (which makes one think twice)and don't silently inject evil reinterpret_casts (see Item 92).

Consider the following code, where Derived inherits from Base:



extern void Fun( Derived* );





void Gun( Base* pb ) {


 // let's assume Gun knows for sure pb actually points to a Derived


 // and wants to forward to Fun


 Derived* pd = (Base*)pb;                     // bad: C-style cast


 Fun( pd );


}



If Gun has access to the definition of Derived (say by including "derived.h"), the compiler will have the necessary object layout information to make any needed pointer adjustments when casting from Base to Derived. But say Gun's author forgets to #include the appropriate definition file, and Gun only sees a forward declaration of class Derived;. In that case, the compiler will just assume that Base and Derived are unrelated types, and will reinterpret the bits that form Base* as a Derived*, without making any necessary adjustments dictated by object layout!

In short, if you forget to #include the definition, your code crashes mysteriously, even though it compiles without errors. Avoid the problem this way:



extern void Fun( Derived* );





void Gun( Base* pb ) {


 // if we know for sure that pb actually points to a Derived, use:


 Derived* pd = static_cast<Derived*>(pb);    // good: C++-style cast


 // otherwise: = dynamic_cast<Derived*>(pb); // good: C++-style cast


 Fun(pd);


}



Now, if the compiler doesn't have enough static information about the relationship between Base and Derived, it will issue an error instead of automatically performing a bitwise (and potentially lethal) reinterpret_cast (see Item 92).

The C++-style casts can protect the correctness of your code during system evolution as well. Say you have an Employee-rooted hierarchy and you need to define a unique employee ID for each Employee. You could define the ID to be a pointer to the Employee itself. Pointers uniquely identify the objects they point to and can be compared for equality, which is exactly what's needed. So you write:



typedef Employee* EmployeeID;





Employee& Fetch( EmployeeID id ) {


 return *id;


}



Say you code a fraction of the system with this design. Later on, it turns out that you need to save your records to a relational database. Clearly, saving pointers is not something that you'd want to do. So, you change the design such that each employee has a unique integral identifier. Then, the integral IDs can be persisted in the database, and a hash table maps the IDs to Employee objects. Now the typedef is:



typedef int EmployeeID;





Employee& Fetch( EmployeeID id ) {


 return employeeTable_.lookup(id);


}



That is a valid design, and you'd expect that all misuses of EmployeeID would be flagged as compile-time errors. And they will, except for this little obscure code:



void TooCoolToUseNewCasts( EmployeeID id ) {


 Secretary* pSecretary = (Secretary*)id;         // bad: C-style cast


 // …


}



With the old typedef, the C-style cast performed a static_cast; with the new one, it performs a reinterpret_cast against some integer, firmly planting the code in the scary realm of undefined behavior (see Item 92).

C++-style casts are also easy to search for with automatic tools such as grep. (No grep regular expression can catch the C cast syntax.) Because casts are very dangerous (especially static_cast of pointers and reinterpret_cast; see Item 92), using automated tools to keep track of them is always a good idea.

    Team LiB
    Previous Section Next Section