Have you ever wanted to learn how basic types of C++ variables interact in complex situations? Ivor Horton explains this, and also describes some interesting features of C++. This article is from chapter 3 of Ivor Horton's Beginning ANSC C++ The Complete Language (Apress, 2004; ISBN 1590592271).

IN THIS CHAPTER, I expand on the types that I discussed in the previous chapter and explain how variables of the basic types interact in more complicated situations. I also introduce some new features of C++ and discuss some of the ways that these are used.

In this chapter you’ll learn

How expressions involving mixed types of data are evaluated

How you can convert a value from one basic type to another

What the bitwise operators are and how you can use them

How you can define a new type that limits variables to a fixed range of possible values

How you can define alternative names for existing data types

What the storage duration of a variable is and what determines it

What variable scope is and what its effects are

Mixed Expressions

You’re probably aware that your computer can only perform arithmetic operations on pairs of values of the same type. It can add two integers, and it can add two floating-point values, but it can’t directly add an integer to a floating-point value. The expression2 + 7.5, for example, can’t be evaluated as it stands because 2 is an integer and 7.5 is a floating-point number.

The only way you can do this calculation is to convert one of the values into the same type as the other—typically, the integer value will be converted to its floating-point equivalent, so the expression will be calculated as 2.0 + 7.5. The same applies to mixed expressions in C++. Each binary arithmetic operation requires both operands to be of the same type; if they’re different, one of them must be converted to the type of the other. Consider the following sequence of statements:

int value1 = 10; long value2 = 25L; float value3 = 30.0f; double result = value1 + value2 + value3; // Mixed calculation

The value of result is calculated as the sum of three different types of variables. For each add operation, one of the operands will be converted to the type of the other before the addition can be carried out. The conversion to be applied, and which operand it applies to, is determined by a set of rules that are checked in sequence until one is found that applies to the operation to be carried out. The preceding statement is actually executed with the following steps:

value1 + value2 is calculated by converting value1 to type long before the addition. The result is also of type long, so the calculation is 10L + 25L = 35L.

The next operation is 35L + value3. The previous result, 35L, is converted to type float before it’s added to value3. The result is of type float, so the operation will be 35.0f + 30.0f = 65.0f.

Finally, the previous result is converted to type double and stored in result.

The rules for dealing with mixed expressions only come into play when the types of the operands for a binary operator are different. These rules, in the sequence in which they’re applied, are as follows:

If either operand is of type long double, the other is converted to long double.

If either operand is of type double, the other is converted to double.

If either operand is of type float, the other is converted to float.

Any operand of type char, signed char, unsigned char, short, or unsigned short is converted to type int, as long as type int can represent all the values of the original operand type. Otherwise, the operand is converted to type unsigned int.

An enumeration type is converted to the first of int, unsigned int, long, or unsigned long that accommodates the range of the enumerators.

If either operand is of type unsigned long, the other is converted to unsigned long.

If one operand is of type long and the other is of type unsigned int, then provided type long can represent all the values of an unsigned int, the unsigned int is converted to type long. Otherwise, both operands are converted to type unsigned long.

If either operand is of type long, the other is converted to type long.

You haven’t seen enumeration types yet, but you’ll look at them little later in this chapter. They appear here so you have the complete set of rules. This all looks rather complicated, but it really isn’t. Some of the apparent complexity arises because the range of values for integer types can be implementation dependent, so the rules need to accommodate that. The compiler checks the rules in sequence until it finds one that applies. If the operands are the same type after applying that rule, then the operation is carried out. If not, another rule is sought.

The basic idea is very simple. With two operands of different types, the type with the lesser range of values is converted to the other. The formal rules roughly boil down to the following:

If the operation involves two different floating-point types, the one with the lesser precision will be promoted to the other.

If the operation involves an integer and a floating-point value, the integer will be promoted to the floating-point type.

If the operation involves mixed integer types, the type with the more limited range will be promoted to the other.

If the operation involves enumeration types, they’ll be converted to a suitable integer type.

The term conversion means an automatic conversion of one type to another. The term promotion generally means a conversion of a data value from a type with a lesser range to a type with a greater range. You’ll see shortly that you can convert explicitly from one data type to another. Such a conversion is referred to as a cast, and the action of explicitly converting a value to a different type is called casting.

Just because C++ supports expressions involving mixed types doesn’t mean it’s a good idea in general. The results are often not what you expect, especially if you mix signed and unsigned types, so you should avoid writing mixed expressions as far as possible.

Assignments and Different Types

If the type of an expression on the right of an assignment operator is different from that of the variable on the left, the result of evaluating the expression on the right side will automatically be converted to the type of the variable on the left before it’s stored. In many cases, you can lose information in this way. For example, suppose you have a floating-point value defined as

double root = 1.732;

If you now write the statement

int value = root;

the conversion of the value of root to int will result in 1 being stored in value. A variable of type int can only store a whole number, so the fractional part of the value stored in root is discarded in the conversion to type int. You can even lose information with an assignment between different types of integers:

long count = 60000; short value = count;

If short is 2 bytes and long is 4 bytes, the former doesn’t have the range to store the value of count, and an incorrect value will result.

Many compilers will detect these kinds of conversions and provide you with a warning message when they occur, but don’t rely on this. To prevent these kinds of problems, you should, as far as possible, avoid assigning a value of one type to a variable of a type with a lesser range of values. Where such an assignment is unavoidable, you can specify the conversion explicitly to demonstrate that it’s no accident and that you really meant to do it. Let’s see how that works.

Explicit Casts

With mixed arithmetic expressions involving the basic types, your compiler automatically arranges conversions of operands where necessary, but you can also force a conversion from one type to another by using an explicit cast.To cast the value of an expression to a given type, you write the cast in the following form:

static_cast<the type to convert to>(expression)

The keyword static_cast reflects the fact that the cast is checked statically—that is, when your program is compiled. Later, when you get to deal with classes, you’ll meet dynamic casts, where the conversion is checked dynamically—that is, when the program is executing. The effect of the cast is to convert the value that results from evaluating expression to the type that you specify between the angled brackets. The expression can be anything from a single variable to a complex expression involving lots of nested parentheses.

Here’s a specific example of the use of static_cast<>():

The initializing value for the variable whole_number is the sum of the integral parts of value1 and value2, so they’re each explicitly cast to type int. The variable whole_number will therefore have the initial value 25. The casts do not affect the values stored in value1 and value2, which will remain as 10.5 and 15.5, respectively. The values 10 and 15 produced by the casts are just stored temporarily for use in the calculation and then discarded. Although both casts cause a loss of information in the calculation, the compiler will always assume that you know what you’re doing when you explicitly specify a cast.

In the situation that I referred to earlier relating to assignments with different types, you can always make it clear that you know the cast is necessary by making it explicit:

int value = static_cast<int> (root);

Generally, the need for explicit casts should be rare, particularly with basic types of data. If you have to include a lot of explicit casts in your code, it’s often a sign that you could choose more suitable types for your variables. Still, there are circumstances when casting is necessary, so let’s look at a simple example of this situation.

This article is excerpted from Beginning ANSI C++ The Complete Language by Ivor Horton (Apress, 2004; ISBN 1590592271). Check it out at your favorite bookstore today. Buy this book now.