Advertisement

Cast with caution

July 31, 2006

Dan_Saks-July 31, 2006

It's almost impossible to write real programs, especially embedded ones, without using a cast here or there. Nonetheless, you should try to use casts as sparingly as you can.

When you declare an object with a particular type, such as int or char *, you're telling the compiler how you intend to use that object later in the program. The compiler can then check that your program uses the object only as intended. This is good. Compile-time type checking turns potential run-time errors into compile-time errors, which are much easier to spot and fix.

For example, when you declare n as an int, you're telling the compiler that you intend to store only integer values into n. You can do arithmetic with n by using it as an operand for operators such as +, -, binary *, and /. However, you can't use n as a pointer by applying the unary * operator, as in:


int n;
...
*n = 3;     // no: can't dereference an int
As another example, when you declare p as a char*, you're telling the compiler that you intend to store pointer values into p. You can use p as a pointer by applying the unary * operator. You can do limited arithmetic on p by using it as an operand of the + and " operators, as in:

int *p;
int i;
double d;
...
p = p + i; // yes: can add pointer + int
p = p + d; // no: can't add pointer + double
Moreover, you can't use p as an operand of the binary * and / operators. In general, a data type describes a set of behaviors for a data object. It tells the compiler what the program can and can't do safely with that object during program execution. The compiler can verify that the program uses the object only in ways permitted by its type, and rejects any program that oversteps that permission. In C and C++, you can use a cast expression to compile code that the compiler's type checking would otherwise reject. When you use a cast, you're telling the compiler "Yes, I know you think this operation is suspicious, and thanks for pointing that out." (It rarely hurts to be polite.) "But I think I know what I'm doing, so please be quiet and let me do the operation anyway." In essence, using a cast nullifies compile-time type checking. Considering how often the compiler is right and you are wrong, you should approach casts very gingerly.

Implicit and explicit conversions
Most programming languages, including C and C++, give compilers some latitude to perform implicit conversions from one type to another. These conversions tend to be pretty safe. Their behavior is sufficiently predictable and portable that it's okay to let programs perform them implicitly. For example, in:


int i;
double d;
...
d = d + i;
the expression d + i converts i to double before adding d and i. In truth, the program doesn't change i into a double. Rather, it creates a temporary double object (possibly in a register) initialized with the converted value of i, and then adds d and the temporary. While the compiler views some conversions (for example, int to double) as safe, it treats other conversions with suspicion. Suspicious conversions may work as expected on some platforms, but might lead to different results or outright errors on other platforms. Since these conversions can be useful at times, we still want them available; we just don't want them sneaking into our programs unannounced. Therefore, programs can use these conversions only if they do so explicitly by using cast expressions. In C and C++, a cast expression has the form (T)e, which converts expression e to type T. For example, converting a "pointer to char" into a "pointer to int", as in:

char *pc;
int *pi;
...
pi = pc;    // questionable conversion
can cause memory alignment errors, so your compiler should complain about it. You can quell the complaint by using a cast, as in:

pi = (int *)pc;     // ok?
You should be aware that although the compiler may have stopped complaining, the conversion is still suspect. Still other conversions aren't just suspect–they're either meaningless or dangerous, and shouldn't be allowed. Consequently, programs can't do them even with a cast. For example, you can't convert a double into a pointer, as in:

char *p;
double d;
...
p = d;          // no: invalid conversion
p = (char *)d;  // still no
A cast is a sledgehammer, but not a pile driver.

"New-style" casts in C++
In C, a cast expression has only one form, namely (T)e, which converts expression e to type T. In addition to this "C-style" cast, C++ offers two more alternatives:

a "function-style" cast of the form T(e), a "new-style" cast with any of the forms:

const_cast<T>(e)
reinterpret_cast<T>(e)
static_cast<T>(e)
The function-style cast T(e) has the exact same semantics as the C-style cast (T)e. The difference is purely syntactic. Like the functional-style cast, the new-style casts don't provide any additional conversion functionality beyond C-style casts. Rather, new-style casts divvy the functionality of C-style casts into cast operators with more distinct behavior. As a bonus, each new-style cast offers better compile-time feedback as to whether the cast is likely to do what you hope it does. The const_cast operator is specifically for casting away const or volatile. As I explained in my last column, C and C++ permit qualification conversions such as from "pointer to T" to "pointer to const T".1 Reversing the conversion requires either a C-style cast or a const_cast, as in:

char *p;
char const *pc;
...
p = pc;                      // error
p = (char *)pc;              // ok: C-style cast
p = const_cast<char *>(pc);  // better: new-style cast
Both the C-style cast and the new-style cast yield the same result, but the new-style cast is preferable. Using const_cast<char *>(pc) informs the compiler that the cast is supposed to reverse a qualification conversion. If indeed the cast does anything else, the compiler will complain. Casting away const is hazardous, but using a const_cast makes it a little less so. An explicit conversion that reverses an implicit conversion generally has portable behavior. Other explicit conversions tend to have non-portable behavior. For example, the cast expression in:

double d;
int i;
...
i = (int)d;                 // portable
has the same behavior across all Standard C++ implementations, namely, it converts the value of d to int by discarding d's fractional part. On the other hand, converting an integer value to a pointer type, as in:

dual_timers *const timers = (dual_timers *)0x03FF6000;   // non-portable
is not necessarily portable. As I discussed in an earlier column,2 this is typically something that programs do to communicate with memory-mapped device registers. How the conversion transforms the integer value into a pointer value depends on the target hardware. In general, you can use static_cast<T>(e) for the explicit conversions that you expect to be portable, and reinterpret_cast<T>(e) for those you expect to be non-portable. The compiler will let you know if your understanding of the cast is wrong. For example:

i = static_cast<int>(d);
has the same behavior as:

i = (int)d;
However:

i = reinterpret_cast<int>(d);
provokes a compilation error because the conversion from double to int is portable, and reinterpret_cast is only for non-portable conversions. Similarly, on any given platform:

dual_timers *const timers = reinterpret_cast<dual_timers *> (0x03FF6000);
has the same behavior as:

dual_timers *const timers = (dual_timers *)0x03FF6000;
However, the behavior of both casts could vary across platforms. Therefore:

dual_timers *const timers = static_cast<dual_timers *>(0x03FF6000);
won't compile because static_cast is only for portable conversions. C++ has yet another cast operator, dynamic_cast. I don't regard this as one of the "new-style" casts. It's not a replacement for C-style casts. Rather, it provides entirely different functionality known as run-time type information, which is a subject for another day. The new-style casts make casts easier to spot in source code, both for humans and for search tools such as grep. This is good. Casts are hazardous, and a hazard that's easier to spot is easier to avoid. The new-style casts are available only in C++, but making casts easy to spot would still be good practice even in C. You can mimic the new-style cast notation in C using a macro defined simply as:

#define cast(t) (t)
Then you could rewrite:

p = (char *)pc;
as something that looks more like a C++ new-style cast.

p = cast(char *)(pc);

This has no effect on the code other than to make it easier to see casts or search for them using an automated tool.

More to do
In summary, type checking is good. Casts subvert it. Avoid using casts whenever possible. If you must cast, then favor the new-style casts.

In future columns I'll provide concrete examples of how to minimize your use of casts.

About Dan Saks

Endnotes:
1. Saks, Dan, "Qualifiers in Multilevel Pointers", Embedded Systems Design, March 2006, p. 14.
Back
2. Saks, Dan, "More Ways to Map Memory", Embedded Systems Programming, January 2005, p. 7.
Back

Loading comments...