Code Capsule

Conversion and Casts

Chuck Allison


Chuck Allison is a regular columnist with CUJ and a software architect for the Family History Department of the Church of Jesus Christ of Later Day Saints Church Headquarters in Salt Lake City. He has a B.S. and M.S. in mathematics, has been programming since 1975, and has been teaching and developing in C since 1984. His current interest is object-oriented technology and education. He is a member of X3J16, the ANSI C++ Standards Committee. Chuck can be reached on the Internet at 72640.1507@compuserve.com.

Since C was invented mainly for systems programming, it has features that are "close to the machine." These features include increment and decrement operators (which usually map to a single machine instruction), bitwise operations, pointers and addressing operators, and a rich set of low-level data types. What other language has eight flavors of integers? It is essential for a C/C++ programmer to know how objects of different types interact. A compiler implicitly converts objects from one type to another when different data types are intermixed, such as in arithmetic expressions, or when an object is assigned or passed as an argument to a different type of object. In some cases the conversion merely reinterprets rather than alters the underlying bit pattern (although the conversion may also3 widen or narrow the object). In this article I will explain these implicit standard conversions, user-defined conversions, and the explicit conversion capability available through the cast mechanism, including C++ new-style casts.

Integral Promotion

In an environment where short, int, and long are all distinct sizes, the eight integral types obey the following sequence of inequalities with respect to the maximum value each can represent:

signed char      <unsigned char  <signed short
<unsigned short  <signed int     <unsigned int
<signed long     <unsigned long
In most environments, however, int is equivalent to either a short or a long. In any case, integral expressions operate only on objects of type int and long, or their unsigned varieties. So when an object of an integral type narrower than int occurs in an expression, the compiler promotes it to an int (or unsigned int, if needed) for the computation. Consider the following code fragment:

char cl: 100, c2=200, c3= c1 + c2;
printf("%d\n",c3);
In an environment where a char is an 8-bit byte, this fragment will print the value 44 (see
Listing 1) . The interesting feature of this program is what happens to c2. Why does c2 print as -56 in Listing 1? Except for the char *format specification, the compiler does not type-check arguments to printf, therefore c2 is promoted to int before printf sees it. A compiler is free to implement a plain char as either a signed or unsigned char. As you can see, the compiler I use in this example, Borland C++, uses signed char. A standard-conforming compiler promotes a signed char to an int using sign extension, meaning that it populates the extra high-order bits with the original char's sign bit. A char holding the value 200 (0xC8) has a sign bit (its most significant bit) of 1, so it promotes to an int containing 0xFFC8 (see Listing 2) . The program then calculates the sum

     0064
+    FFC8
  -------
= (1)002C
which truncates to 0x2C or 44.

Listing 3 shows what happens when you use an unsigned char instead. The sum 300 overflows the maximum value a byte can hold. The result equals the overflow amount, which can be expressed as a modulus operation:

44 == 300 % 256
where 256 == UCHAR_HAX + 1. (UCHAR_MAX is defined in <limits.h>).

Listing 4 shows that unsigned chars promote by filling the high-order bits of the new object with zeroes. In this case the program computes the sum

    0064
+   00C8
  ------
=   012C
which truncates to 0x2C, which again is 44.

The difference in promoting signed vs. unsigned quantities can be significant. For example, the "dump" program in Listing 5 prints each byte of its input file as two hexadecimal digits, sixteen per row, followed by the corresponding ASCII representation (see the output in Listing 6) . If I hadn't declared the array buf as unsigned, any byte values greater than or equal to 128 would display with an extra, prepended FF, misaligning the output (recompile and see for yourself).

In C++, objects of enumeration type promote to either int, unsigned int, long, or unsigned long, whichever is the smallest type that can represent all values of the enumeration. Although wchar_t, the "wide character" type, is a distinct, overloadable type in C++, it otherwise behaves as its underlying implemention type, which is one of the integer types. When assigning a value of the new C++ type bool to an integer, false becomes 0 and true becomes 1.

Exercise

Examine the output of the program in Listing 7. The two sides of the assignment

y = x+x;
print differently. Why.? (See answer at the end of this article.)

Demotions

The result of converting an integer to a narrower unsigned type is the remainder of division by 2n, where n is the unsigned type's length in bits. This explains why, in Listing 3, converting 300 to unsigned char resulted in 44. Such a conversion is called a demotion, or equivalently, a narrowing conversion. If the narrower type is signed, however, the result is implementation-defined if the smaller type cannot represent the larger value. As Listing 8 shows, my compiler just truncates high-order bits, if necessary, and interprets the demoted value according to two's-complement arithmetic. For example, the bit pattern for 60000U is

    1110 1010 0110 0000
But if this value is to become a signed integer, it must represent a negative number, because its sign bit is set. To determine the magnitude of this negative integer, the two's-complement rules say to flip the bits and add 1:

   0001 0101 1001 1111
+                    1
   -------------------
=  0001 0101 1010 0000
which is 5536, so (int)60000U == -5536.

The bit pattern for 70000L is:

    0000 0000 0000 0001 0001 0001 0111 0000
The conversion to short (which is the same as int in my 16-bit environment) simply truncates the top sixteen bits. Since its sign bit is not set, this short now represents the positive number 4464.

Converting a floating-point number to an integer discards the fractional part, truncating toward zero. When a floating-point value is too large to fit in an integer, the result is undefined.

As you would expect, when an integer is converted to bool in C++, a non-zero value becomes true, and zero false.

Arithmetic Conversions

Working with floating-point numbers has been likened to moving piles of sand — every time you move one you lose a little. A finite floating point number system can represent only a small portion of the set of real numbers and large integers. When these numbers are involved in calculations, the system substitutes the closest representable number. Listing 9 illustrates this process. The roundoff error inherent in finite-precision arithmetic caused the product 100 * 23.4 to land closer to 2339 than 2340.

Standard C defines three levels of floating-point precision: float (single precision), double (double precision), and long double (extended precision). The set of representable float values is a subset of representable doubles, which in turn is a subset of representable long double numbers. C interprets unadorned constants, such as 100.0, as type double. To force a constant to be of another floating type, you can use a suitable suffix, as Table 1 illustrates.

Demoting a floating-point number to another floating-point type of smaller precision, (as in double to float), produces three possible outcomes, depending on the value of the number being demoted:

1) The number being demoted (the source number) is outside the target type's range. In this case, the result is defined.

2) The source number is within the target type's range but is not representable by the target type. The result is the nearest higher or lower value, depending on the rounding algorithm used (which is implementation -defined).

3) The source number is exactly representable in the target precision, in which case there is no error.

The program in Listing 10 shows that ULONG_MAX, the largest unsigned long integer (4,294,967,295 in my 16-bit environment, is not representable in my compiler's set of floats, but ULONG_MAX1, is because it's a power of two, and the floating-point number system is binary-based. Both double and long double have sufficient precision to represent ULONG_MAX, however.

When two numbers of different types appear as operands in an arithmetic operation, the compiler balances the expression by either converting one type to the other, or, in the case of small integers, converting both to a common type. If either operand is of floating-point type, then the compiler converts the narrower type object to the larger floating-point type. If both arguments are integers, they undergo integral promotion so that the operands temporarily become either int or long (or their unsigned versions, as appropriate) — then the object of narrower type is converted to the wider type for the operation. In other words, C and C++ promote the types used in a dual operand numeric operation upward within one of the following prioritized lists, depending on the relationship between long and int:

If sizeof(long) > sizeof(int):
-----------------------------
long double
double
float
unsigned long
unsigned int
int

If sizeof(long) == sizeof(int):
------------------------------
long double
double
float
unsigned long == unsigned int
long == int

Function Prototypes

Function prototypes do more than just type-check function arguments at compile time. When necessary, they also automatically convert arguments to the type of the corresponding parameter. For example, a common error unchecked by old-style function definitions (i.e., without prototypes) is to pass an integer argument to a double parameter:

   f(1);
   /* Big trouble! f expects a double */
   ...

void f(x)
double x;
{
   ...
As
Listing 11 illustrates, providing a fully-prototyped declaration of such a function before you use it forces an implicit conversion from int to double. Listing 11 also shows that a narrowing conversion, in this case from double to int, can be dangerous (because the integer representation of 1.0e6 is undefined in a 16bit environment).

You are responsible for unchecked arguments. For example, in the following prototype for printf, the ellipsis specificationindicates that any number of arguments of any type may follow the first parameter:

printf(const char *,...);
Since the compiler has no way of knowing what these should be, it can't catch the following error:

#include <stdio.h>

main()
{

   int x = 1;
   printf("%ld\n",x);
   /*Mismatch!*/
   return 0;
}

/* Output:
65537
*/
The compiler can perform only default promotions on unchecked arguments, which consist of integral promotion and converting type float to double.

Explicit Conversions

The conversions discussed so far are all implicit conversions — they happen without any intervention on your part. You can explicitly request a conversion at any time with a cast operator. For example, in the following assignment, the fractional part of x has no effect:

int i;
double x;
/*...*/
i=i+x;
However, the code runs through the following laborious sequence:

1. Convert i to a double.

2. Add (double)i to x using double arithmetic.

3. Truncate the result to an integer and assign to i.

If you're counting nanoseconds, you can avoid the double arithmetic by casting x to an int:

i = i + (int)x;
Casts can come in handy when dealing with pointers. If you need to inspect the bytes of an integer, for example, you can do this:

int i;
char *cp = (char *) &i;
char hi_byte = *cp++; /* if big-endian*/
char lo_byte = *cp;
/* etc. */
The cast is necessary to convince the compiler that you know what you're doing, since it normally doesn't allow mixing pointers to different types. C does not require casts when assigning to and from pointers to void, as is illustrated by the function in
Listing 12, which inspects the bytes of any object (sample output is in Listing 13) . However, in C++, it is an error to assign from a void pointer without a cast. To compile Listing 12 as a C++ program, change the declaration of p to:

const unsigned char *p = (const unsigned char *) ptr;
C++ also allows function-style casts. For example, you can rewrite the statement

i = i + (int)x;
as

i = i + int(x);
Besides being easier on the eye, this notation is compatible with the use of constructors for creating temporaries and initializing objects, as in:

#include "foo.h"      //defines class Foo
Foo f: Foo(1) + Foo(2);
In this case, class Foo must have a constructor that takes a single integer argument. See the section on user-defined conversions below for more detail.

One drawback to function-style casts is that they allow only single-token type names. For example, the following statement is illegal:

const unsigned char *p = const unsigned char *(p);
The work-around for this problem is to use a typedef:

typedef unsigned char *const_ucharp;
const unsigned char *p = const_ucharp(p);

Const Correctness

The const qualifier is a critical component of the C type system, and even more so for C++. Whenever you create a read-only pointer or read-only reference function parameter you should declare it const. Consider the following implementation of memcpy:

void *memcpy(void *to. const void *from, size_t n)
{
   char *to_p = to;
   const char *from_p = from;
   while (n--)
      *top++: *from_p++;
   return to;
}
The source buffer pointer (from) is declared as a pointer to const, meaning that the function has no intention of altering what it points at. This declaration not only guards against inadvertent assignment within the function, but allows you to pass a pointer to const as an argument. If the const qualifier were missing in the definition of memcpy, then you could not pass t as an argument in the following:

char *s = ... ;
const char *t = ... ;
memcpy(s,t,10);
The compiler needs a guarantee that memcpy will not try to modify what t points at. With const in the prototype, you can use pointers to both const and non-const.

All of the above applies to references as well as pointers. A common idiom for passing objects by value in C++ is to pass a const reference:

void g(const Foo& f);
This guarantees that g will not modify f.

You can also declare that a pointer itself cannot change by placing const after the asterisk:

char * const p;
*p = 'a'; // OK, only the
        //pointer is const
++p; // Error, can't
    //modify pointer
To disallow modification of both the pointer and the contents referenced, use const in both places:

const char * const p;
char c;
c = *p; // OK - can read contents
*p = 'a';        // Error
++p;             // Error
A const member function in a C++ class promises not to modify the object to which it applies. For example, a date class might have separate functions to inspect and to alter the number of the month:

class Date
{
   int month_;
   // rest of implementation omitted
public:
   int month() const; // return month
   void month(int);  // change month
   // etc.
};
The observer function is declared const, because it just returns the current value. Therefore, both of the following function invocations are valid:

const Date d1;
Date d2;
d1.month();
d2.month ();
But you can't change d1 's month, as in

d1.month(10);
because you can't call a non-const member function for a const object. The compiler detects the error by inspecting the type of the object's hidden this pointer. When the object is non-const, the type of this is

Date * const;
meaning "this is a const pointer to Date." So you can't alter this (that would be weird, anyway), but you can alter what it points to, therefore you can change its private data within a non-const member function. On the other hand, if the underlying object is const, then this has the following declaration:

const Date * const this;
which disallows modifying any members. Const member functions expect such a this pointer, so if you invoke one with a non-const object, there is a mismatch and the compiler complains.

Nevertheless, sometimes you may want to alter a data member within a const member function. For example, a function may not need to alter anything that the user is aware of, but may want to update some private state variable (this situation is common in classes that cache values). You still want to enable the user to invoke the function for const members, since s/he doesn't need to know about your optimization secrets. This use of the const concept is sometimes called semantic const-ness (as opposed to bitwise const-ness). Consider the following class definition, for example:

class B
{
public:
   void f() const;
private:
   int state;
};
To allow B::f to alter state, and still be called for const objects, you "cast away const" in the implementation of f:

void B::f() const
{
   ((B*)this)->state = ...;
   ...
}
Recall that the type of this in f is const B * const. The expression (B*)this suspends the const-ness temporarily, allowing you to make the assignment.

User-Defined Conversions

A class constructor that takes a single argument defines an implicit conversion from the type of that argument to the type of the class. For example, the rational number class (Listing 14, Listing 15, Listing 16, and Listing 17) has the following constructor:

Rational(int n = 0, int d = 1);
which allows zero, one, or two arguments when initializing a rational number. The declaration above ensures that whenever an integer appears in an expression with a Rational object, the integer is implicitly converted to a Rational with a denominator of 1. This implicit conversion allows statements such as the following to be meaningful:

Rational y(4);
cout << 2 / y << endl;
When the compiler sees the expression

2/y
it looks for a global function with signature operator/(int, Rational). Failing that, it notices the existence of the function operator/(const Rational&,const Rational&), in addition to the built-in operator/(int, int). The compiler therefore looks for a conversion of either int to Rational, or Rational to int. If both conversions existed (which they don't here), the compiler would complain, since it wouldn't know which one to pick. Because class Rational allows a constructor with a single integer argument, the compiler uses that to convert 2 to Rational(2), and then invokes operator/(Rational (2),y).

It is significant that operator/(const Rational&, const Rational&) is a global function. If I had defined operator/as a member function, instead, as in

Rational Rational::operator/(const Rational&)
then the compiler would be able to evaluate the expression y/2, but not 2/y, because member operator functions require the left operand to be an object of their class. You could of course provide functions to service all possible combinations of operands:

// These handle r/r, r/i, and i/r, resp.
Rational Rational::operator/(const Rational&)
Rational Rational:: operator/(int);
friend Rational operator/(int, const Rational&);
This strategy requires no conversions at all from int to Rational, but can be quite tedious in classes with a large number of operators. Defining global operator functions in conjunction with appropriate constructors allows symmetric, mixed-mode binary operations with minimum fuss.

Although you don't want to in this case (because of the ambiguity mentioned above), how would you define an implicit conversion from Rational to int? There is no int class definition available to which you can add single-argument constructors whenever you need a conversion from a new type to int. As an alternative, you need some way within a source class definition to provide a conversion from the source class type to other types, even to built-in ones. You can do this conversion with a conversion operator function. To implicitly convert objects of a source class to target type T, just define the following function as a member of the source class:

operator T() const;
For an implicit Rational-to-int conversion, you would add the following to class Rational:

// Return integer part:
operator int() const {return hum/den;}
In general, you don't want implicit conversions going both ways, since the compiler will not know which function to choose for a dual-operand operation. If you feel you must have a conversion from Rational to int, make it an explicit conversion with a member function, such as

int to_int() const {return num/den;}
and call it when you need it, as in

int i : r.to_int();
String classes sometimes include a char* conversion operator; this operator allows string objects to be used as arguments to the ANSI-C string functions that require a char* argument, for example

string s;
strcpy(s, "hello");
Listing 18 shows that you still must be careful about using such conversions with functions with unchecked arguments. The expression

(char *) s
invokes string::operator char* and returns a pointer to data, as expected. But since the second and subsequent arguments to printf are unchecked, no implicit conversion takes place in the second invocation of printf.

Beefing-Up operator[]

Most string class implementations include a member function with the following signature:

char& operator[](size_t);
operator[]'s implementation usually goes something like this:

char& string::operator[](size_t pos)
{
   return data[pos];
}
where data is the underlying array of characters. This member function provides the typical subscripting action that no programmer can live without. A common use:

string s;
char c: s[0];
Because string::operator[] returns a reference, it can be used as an lvalue, as in the assignment

s[0] = 'a';
What if different actions are required, depending on whether the subscript is on the left-hand vs. the right-hand side of the assignment? Such is the case with the bitstring class in the standard C++ library (see
Listing 19, Listing 20, and Listing 21; also "Bit Handling in C++, Part 1," CUJ, December 1993). An expression such as:

bitstring b;
int n = b[0];
stores either a one or a zero in n, and is equivalent to

int n = b.test(0);
The operation

b[0] = n;
either sets or resets the zeroth-bit of b, and is equivalent to

if (n)
   b.set(n);
else
   b.reset(0);
Since the two operations are quite different, we need some way of distinguishing when the subscripting occurs on the left vs. the right side of an assignment. The solution is to introduce a new class, which I call bitref, with only two public member functions: an assignment operator and an implicit integer conversion. Class bitref, which I have nested within the bitstring class, privately maintains a reference to the bitstring it will index, and the index position itself. The constructor is private so only a bitstring object can create a temporary bitref object (bitstring is a friend to bitref.)

The function bitstring::operator[] just returns a temporary bitref object — nothing more. Therefore, the assignment

int n = b[i];
which is equivalent to

int n = b.operator[](i);
becomes

int n = bitref(b,i);
The compiler naturally looks for a conversion from bitref to int, which it finds in bitstring::bitref::operator int(), which returns either a zero or a one for the assignment to n.

For the lvalue case, the statement

b[i] = n;
becomes

bitref(b,i) = n;
which in turn invokes

bitref(b,i).operator=(n);
which either sets or resets the ith bit of b, depending on the value of n.

New-Style Casts

A cast is your way of telling the compiler to allow you to do something potentially dangerous, because you think you know what you're doing. Too often, though, you don't know what you're doing (no offense!), and you spend days tracking down an insidious pointer conversion error or similar bug. It's interesting that programmers use a single cast notation for so many different purposes:

The new-style casts in C++ attempt to minimize the chances for error by categorizing the different types of operations for which you typically use casts. If you cast incorrectly, C++ tells you, either at compile time or run time, depending on the situation.

C++ defines four new cast mechanisms: dynamic_cast, static_cast, reinterpret_cast, and const_cast.

A dynamic_cast provides safe downcasts. You can always do an upcast, i.e,, you can always cast a pointer to derived class to a pointer to a base class, as in:

class B {...};
class D =public B {...} ;
D d;
B* bp = (B*) &d;
This is safe because a D is a B. But casting from a B* to a D* only makes sense if you know that the referenced object is in fact a D. This type of pointer conversion is called a downcast because it travels "down" the class hierarchy (because most people draw base classes at the top of a class diagram). To determine whether or not such a cast is safe requires run-time type information (RTTI). A C++ compiler keeps run-time information for objects of all polymorphic types (i.e., types that have at least one virtual function). The dynamic_cast operator returns a null pointer if the downcast is unsafe, otherwise it returns the original pointer cast to the new type, as in:

D* dp = dynamic_cast<B*> bp;
if (dp)
   // okay to use dp for stuff
   // specific to a D object.
Note that all of the new-style casts use template-like bracket notation for the target type.

The other three new-style cast operators are compile-time mechanisms. You use static_cast when you really do know that a B* points to a D*, so run-time checking isn't necessary. You also use it for invoking standard and user-defined conversions explicitly, which probably makes static_cast the most widely used category of casts for code that is being migrated from C.

reinterpret_cast is the "don't ask, don't tell" cast — it is for doing everything except navigating a class hierarchy (which it does not allow). You can use it to convert any type to any other, for reasons best kept to yourself. It is just about as unsafe as traditional cast notation, except that you're not likely to use it much due to the presence of the other three newstyle casts, and because it sticks out like a sore thumb:

int j;
int *ip = &j
char *cp = reinterpret_cast<char *>(ip);
// trust me!
const_cast, as you may have surmised, makes abundantly clear your intentions to cast-away const:

void B::f() const
{
// Modify data member of a const object:
   (const_cast<B*>(this))->state =
...;
   ...
}
The target type can drop a const or volatile qualifier that is part of the argument type's definition. For example, in the above statement this is of type const B*, but the target type is mere B*. However, in all other respects the target and argument types must match.

Summary

Standard C defines a host of implicit conversions to allow convenient intermixing of its varied types in expressions. I have tried to show how these conversions work and when they can get you into trouble. With single-argument constructors and conversion operators, C++ allows user-defined types to behave as much as possible like built-in types, but there are still some "gotchas." One of C's biggest gotchas is its potentially unsafe casting mechanism. When the new-style casts become available in commercial compilers, a whole heap of debugging headaches should disappear.

Answer to Exercise

Since the value of x is 0x7C (124), in the expression

x + x
both occurrences of x are promoted to 0x007C, which add to 0x00F8 — no further conversion takes place. y's value of 0xF8, however, promotes to 0xFFF8, because its high-order bit is set.