Section 5.12. Type Conversions


5.12. Type Conversions

The type of the operand(s) determine whether an expression is legal and, if the expression is legal, determines the meaning of the expression. However, in C++ some types are related to one another. When two types are related, we can use an object or value of one type where an operand of the related type is expected. Two types are related if there is a conversion between them.

As an example, consider

      int ival = 0;      ival = 3.541 + 3; // typically compiles with a warning 

which assigns 6 to ival.

The operands to the addition operator are values of two different types: 3.541 is a literal of type double, and 3 is a literal of type int. Rather than attempt to add values of the two different types, C++ defines a set of conversions to transform the operands to a common type before performing the arithmetic. These conversions are carried out automatically by the compiler without programmer interventionand sometimes without programmer knowledge. For that reason, they are referred to as implicit type conversions.

The built-in conversions among the arithmetic types are defined to preserve precision, if possible. Most often, if an expression has both integral and floating-point values, the integer is converted to floating-point. In this addition, the integer value 3 is converted to double. Floating-point addition is performed and the result, 6.541, is of type double.

The next step is to assign that double value to ival, which is an int. In the case of assignment, the type of the left-hand operand dominates, because it is not possible to change the type of the object on the left-hand side. When the left- and right-hand types of an assignment differ, the right-hand side is converted to the type of the left-hand side. Here the double is converted to int. Converting a double to an int TRuncates the value; the decimal portion is discarded. 6.541 becomes 6, which is the value assigned to ival. Because the conversion of a double to int may result in a loss of precision, most compilers issue a warning. For example, the compiler we used to check the examples in this book warns us:

      warning: assignment to 'int' from 'double' 

To understand implicit conversions, we need to know when they occur and what conversions are possible.

5.12.1. When Implicit Type Conversions Occur

The compiler applies conversions for both built-in and class type objects as necessary. Implicit type conversions take place in the following situations:

  • In expressions with operands of mixed types, the types are converted to a common type:

          int ival;      double dval;      ival >= dval // ival converted to double 

  • An expression used as a condition is converted to bool:

          int ival;      if (ival)   // ival converted to bool      while (cin) // cin converted to bool 

    Conditions occur as the first operand of the conditional (?:) operator and as the operand(s) to the logical NOT (!), logical AND (&&), and logical OR (||) operators. Conditions also appear in the if, while, for, and do while statements. (We cover the do while in Chapter 6)

  • An expression used to initialize or assign to a variable is converted to the type of the variable:

          int ival = 3.14; // 3.14 converted to int      int *ip;      ip = 0; // the int 0 converted to a null pointer of type int * 

In addition, as we'll see in Chapter 7, implicit conversions also occur during function calls.

5.12.2. The Arithmetic Conversions

The language defines a set of conversions among the built-in types. Among these, the most common are the arithmetic conversions, which ensure that the two operands of a binary operator, such as an arithmetic or logical operator, are converted to a common type before the operator is evaluated. That common type is also the result type of the expression.

The rules define a hierarchy of type conversions in which operands are converted to the widest type in the expression. The conversion rules are defined so as to preserve the precision of the values involved in a multi-type expression. For example, if one operand is of type long double, then the other is converted to type long double regardless of what the second type is.

The simplest kinds of conversion are integral promotions. Each of the integral types that are smaller than int char, signed char, unsigned char, short, and unsigned shortis promoted to int if all possible values of that type fit in an int. Otherwise, the value is promoted to unsigned int. When bool values are promoted to int, a false value promotes to zero and true to one.

Conversions between Signed and Unsigned Types

When an unsigned value is involved in an expression, the conversion rules are defined to preserve the value of the operands. Conversions involving unsigned operands depend on the relative sizes of the integral types on the machine. Hence, such conversions are inherently machine dependent.

In expressions involving shorts and ints, values of type short are converted to int. Expressions involving unsigned short are converted to int if the int type is large enough to represent all the values of an unsigned short. Otherwise, both operands are converted to unsigned int. For example, if shorts are a half word and ints a word, then any unsigned value will fit inside an int. On such a machine, unsigned shorts are converted to int.

The same conversion happens among operands of type long and unsigned int. The unsigned int operand is converted to long if type long on the machine is large enough to represent all the values of the unsigned int. Otherwise, both operands are converted to unsigned long.

On a 32-bit machine, long and int are typically represented in a word. On such machines, expressions involving unsigned ints and longs are converted to unsigned long.

Conversions for expressions involving signed and unsigned int can be surprising. In these expressions the signed value is converted to unsigned. For example, if we compare a plain int and an unsigned int, the int is first converted to unsigned. If the int happens to hold a negative value, the result will be converted as described in Section 2.1.1 (p. 36), with all the attendant problems discussed there.

Understanding the Arithmetic Conversions

The best way to understand the arithmetic conversions is to study lots of examples. In most of the following examples, either the operands are converted to the largest type involved in the expression or, in the case of assignment expressions, the right-hand operand is converted to the type of the left-hand operand:

      bool      flag;         char           cval;      short     sval;         unsigned short usval;      int       ival;         unsigned int   uival;      long      lval;         unsigned long  ulval;      float     fval;         double         dval;      3.14159L + 'a'; // promote 'a' to int, then convert to long double      dval + ival;    // ival converted to double      dval + fval;    // fval converted to double      ival = dval;    // dval converted (by truncation) to int      flag = dval;    // if dval is 0, then flag is false, otherwise true      cval + fval;    // cval promoted to int, that int converted to float      sval + cval;    // sval and cval promoted to int      cval + lval;    // cval converted to long      ival + ulval;   // ival converted to unsigned long      usval + ival;   // promotion depends on size of unsigned short and int      uival + lval;   // conversion depends on size of unsigned int and long 

In the first addition, the character constant lowercase 'a' has type char, which as we know from Section 2.1.1 (p. 34) is a numeric value. The numeric value that 'a' represents depends on the machine's character set. On our ASCII machine, 'a' represents the number 97. When we add 'a' to a long double, the char value is promoted to int and then that int value is converted to a long double. That converted value is added to the long double literal. The other interesting cases are the last two expressions involving unsigned values.

5.12.3. Other Implicit Conversions

Pointer Conversions

In most cases when we use an array, the array is automatically converted to a pointer to the first element:

      int ia[10];    // array of 10 ints      int* ip = ia;  // convert ia to pointer to first element 

The exceptions when an array is not converted to a pointer are: as the operand of the address-of (&) operator or of sizeof, or when using the array to initialize a reference to the array. We'll see how to define a reference (or pointer) to an array in Section 7.2.4 (p. 240).

There are two other pointer conversions: A pointer to any data type can be converted to a void*, and a constant integral value of 0 can be converted to any pointer type.

Conversions to bool

Arithmetic and pointer values can be converted to bool. If the pointer or arithmetic value is zero, then the bool is false; any other value converts to TRue:

      if (cp) /* ... */     // true if cp is not zero      while (*cp) /* ... */ // dereference cp and convert resulting char to bool 

Here, the if converts any nonzero value of cp to TRue. The while dereferences cp, which yields a char. The null character has value zero and converts to false. All other char values convert to true.

Arithmetic Type and bool Conversions

Arithmetic objects can be converted to bool and bool objects can be converted to int. When an arithmetic type is converted to bool, zero converts as false and any other value converts as TRue. When a bool is converted to an arithmetic type, true becomes one and false becomes zero:

      bool b = true;      int ival = b;   // ival == 1      double pi = 3.14;      bool b2 = pi;   // b2 is true      pi = false;     // pi == 0 

Conversions and Enumeration Types

Objects of an enumeration type (Section 2.7, p. 62) or an enumerator can be automatically converted to an integral type. As a result, they can be used where an integral value is requiredfor example, in an arithmetic expression:

      // point2d is 2, point2w is 3, point3d is 3, point3w is 4      enum Points { point2d = 2, point2w,                    point3d = 3, point3w };      const size_t array_size = 1024;      // ok: pt2w promoted to int      int chunk_size = array_size * pt2w;      int array_3d = array_size * point3d; 

The type to which an enum object or enumerator is promoted is machine-defined and depends on the value of the largest enumerator. Regardless of that value, an enum or enumerator is always promoted at least to int. If the largest enumerator does not fit in an int, then the promotion is to the smallest type larger than int (unsigned int, long or unsigned long) that can hold the enumerator value.

Conversion to const

A nonconst object can be converted to a const object, which happens when we use a nonconst object to initialize a reference to const object. We can also convert the address of a nonconst object (or convert a nonconst pointer) to a pointer to the related const type:

      int i;      const int ci = 0;      const int &j = i;   // ok: convert non-const to reference to const int      const int *p = &ci; // ok: convert address of non-const to address of a const 

Conversions Defined by the Library Types

Class types can define conversions that the compiler will apply automatically. Of the library types we've used so far, there is one important conversion that we have used. When we read from an istream as a condition

      string s;      while (cin >> s) 

we are implicitly using a conversion defined by the IO library. In a condition such as this one, the expression cin >> s is evaluated, meaning cin is read. Whether the read succeeds or fails, the result of the expression is cin.

The condition in the while expects a value of type bool, but it is given a value of type istream. That istream value is converted to bool. The effect of converting an istream to bool is to test the state of the stream. If the last attempt to read from cin succeeded, then the state of the stream will cause the conversion to bool to be truethe while test will succeed. If the last attempt failedsay because we hit end-of-filethen the conversion to bool will yield false and the while condition will fail.

Exercises Section 5.12.3

Exercise 5.31:

Given the variable definitions on page 180, explain what conversions take place when evaluating the following expressions:

      (a) if (fval)      (b) dval = fval + ival;      (c) dval + ival + cval; 

Remember that you may need to consider associativity of the operators in order to determine the answer in the case of expressions involving more than one operator.


5.12.4. Explicit Conversions

An explicit conversion is spoken of as a cast and is supported by the following set of named cast operators: static_cast, dynamic_cast, const_cast, and reinterpret_cast.

Although necessary at times, casts are inherently dangerous constructs.



5.12.5. When Casts Might Be Useful

One reason to perform an explicit cast is to override the usual standard conversions. The following compound assignment

      double dval;      int ival;      ival *= dval; // ival = ival * dval 

converts ival to double in order to multiply it by dval. That double result is then truncated to int in order to assign it to ival. We can eliminate the unnecessary conversion of ival to double by explicitly casting dval to int:

      ival *= static_cast<int>(dval); // converts dval to int 

Another reason for an explicit cast is to select a specific conversion when more than one conversion is possible. We will look at this case more closely in Chapter 14.

5.12.6. Named Casts

The general form for the named cast notation is the following:

      cast-name<type>(expression); 

cast-name may be one of static_cast, const_cast, dynamic_cast, or reinterpret_cast. type is the target type of the conversion, and expression is the value to be cast. The type of cast determines the specific kind of conversion that is performed on the expression.

dynamic_cast

A dynamic_cast supports the run-time identification of objects addressed either by a pointer or reference. We cover dynamic_cast in Section 18.2 (p. 772).

const_cast

A const_cast, as its name implies, casts away the constness of its expression. For example, we might have a function named string_copy that we are certain reads, but does not write, its single parameter of type char*. If we have access to the code, the best alternative would be to correct it to take a const char*. If that is not possible, we could call string_copy on a const value using a const_cast:

      const char *pc_str;      char *pc = string_copy(const_cast<char*>(pc_str)); 

Only a const_cast can be used to cast away constness. Using any of the other three forms of cast in this case would result in a compile-time error. Similarly, it is a compile-time error to use the const_cast notation to perform any type conversion other than adding or removing const.

static_cast

Any type conversion that the compiler performs implicitly can be explicitly requested by using a static_cast:

      double d = 97.0;      // cast specified to indicate that the conversion is intentional      char ch = static_cast<char>(d); 

Such casts are useful when assigning a larger arithmetic type to a smaller type. The cast informs both the reader of the program and the compiler that we are aware of and are not concerned about the potential loss of precision. Compilers often generate a warning for assignments of a larger arithmetic type to a smaller type. When we provide the explicit cast, the warning message is turned off.

A static_cast is also useful to perform a conversion that the compiler will not generate automatically. For example, we can use a static_cast to retrieve a pointer value that was stored in a void* pointer (Section 4.2.2, p. 119):

      void* p = &d; // ok: address of any data object can be stored in a void*      // ok: converts void* back to the original pointer type      double *dp = static_cast<double*>(p); 

When we store a pointer in a void* and then use a static_cast to cast the pointer back to its original type, we are guaranteed that the pointer value is preserved. That is, the result of the cast will be equal to the original address value.

reinterpret_cast

A reinterpret_cast generally performs a low-level reinterpretation of the bit pattern of its operands.

A reinterpret_cast is inherently machine-dependent. Safely using reinterpret_cast requires completely understanding the types involved as well as the details of how the compiler implements the cast.



As an example, in the following cast

      int *ip;      char *pc = reinterpret_cast<char*>(ip); 

the programmer must never forget that the actual object addressed by pc is an int, not a character array. Any use of pc that assumes it's an ordinary character pointer is likely to fail at run time in interesting ways. For example, using it to initialize a string object such as

      string str(pc); 

is likely to result in bizarre run-time behavior.

The use of pc to initialize str is a good example of why explicit casts are dangerous. The problem is that types are changed, yet there are no warnings or errors from the compiler. When we initialized pc with the address of an int, there is no error or warning from the compiler because we explicitly said the conversion was okay. Any subsequent use of pc will assume that the value it holds is a char*. The compiler has no way of knowing that it actually holds a pointer to an int. Thus, the initialization of str with pc is absolutely correctalbeit in this case meaningless or worse! Tracking down the cause of this sort of problem can prove extremely difficult, especially if the cast of ip to pc occurs in a file separate from the one in which pc is used to initialize a string.

Advice: Avoid Casts

By using a cast, the programmer turns off or dampens normal type-checking (Section 2.3, p. 44). We strongly recommend that programmers avoid casts and believe that most well-formed C++ programs can be written without relying on casts.

This advice is particularly important regarding use of reinterpret_casts. Such casts are always hazardous. Similarly, use of const_cast almost always indicates a design flaw. Properly designed systems should not need to cast away const. The other casts, static_cast and dynamic_cast, have their uses but should be needed infrequently. Every time you write a cast, you should think hard about whether you can achieve the same result in a different way. If the cast is unavoidable, errors can be mitigated by limiting the scope in which the cast value is used and by documenting all assumptions about the types involved.


5.12.7. Old-Style Casts

Prior to the introduction of named cast operators, an explicit cast was performed by enclosing a type in parentheses:

      char *pc = (char*) ip; 

The effect of this cast is the same as using the reinterpret_cast notation. However, the visibility of this cast is considerably less, making it even more difficult to track down the rogue cast.

Standard C++ introduced the named cast operators to make casts more visible and to give the programmer a more finely tuned tool to use when casts are necessary. For example, nonpointer static_casts and const_casts tend to be safer than reinterpret_casts. As a result, the programmer (as well as readers and tools operating on the program) can clearly identify the potential risk level of each explicit cast in code.

Although the old-style cast notation is supported by Standard C++, we recommend it be used only when writing code to be compiled either under the C language or pre-Standard C++.



The old-style cast notation takes one of the following two forms:

      type (expr); // Function-style cast notation      (type) expr; // C-language-style cast notation 

Depending on the types involved, an old-style cast has the same behavior as a const_cast, a static_cast, ora reinterpret_cast. When used where a static_cast or a const_cast would be legal, an old-style cast does the same conversion as the respective named cast. If neither is legal, then an old-style cast performs a reinterpret_cast. For example, we might rewrite the casts from the previous section less clearly using old-style notation:

      int ival; double dval;      ival += int (dval); // static_cast: converts double to int      const char* pc_str;      string_copy((char*)pc_str); // const_cast: casts away const      int *ip;      char *pc = (char*)ip; // reinterpret_cast: treats int* as char* 

The old-style cast notation remains supported for backward compatibility with programs written under pre-Standard C++ and to maintain compatibility with the C language.

Exercises Section 5.12.7

Exercise 5.32:

Given the following set of definitions,

      char cval;  int ival;   unsigned int ui;      float fval;             double dval; 

identify the implicit type conversions, if any, taking place:

      (a) cval = 'a' + 3;        (b) fval = ui - ival * 1.0;      (c) dval = ui * fval;      (d) cval = ival + fval + dval; 

Exercise 5.33:

Given the following set of definitions,

      int ival;                         double dval;      const string *ps;    char *pc;    void *pv; 

rewrite each of the following using a named cast notation:

      (a) pv = (void*)ps;     (b) ival = int(*pc);      (c) pv = &dval;         (d) pc = (char*) pv; 




C++ Primer
C Primer Plus (5th Edition)
ISBN: 0672326965
EAN: 2147483647
Year: 2006
Pages: 223
Authors: Stephen Prata

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net