Previously, you learned that a value of a variable is stored as a sequence of bits, and the data type of the variable tells the compiler how to interpret those bits into meaningful values. Different data types may represent the “same” number differently -- for example, the integer value 3 and the float value 3.0 are stored as completely different binary patterns.
So what happens when we do something like this?
1 |
float f{ 3 }; // initialize floating point variable with int 3 |
In such a case, the compiler can’t just copy the bits representing the int
value 3
into float
variable f
. Instead, it needs to convert the integer value 3
to a floating point number, which can then be assigned to float
variable f
.
The process of converting a value from one data type to another is called a type conversion. Type conversions
can happen in many different cases:
- When assigning to or initializing a variable with a value of a different data type:
1 2 |
double d{ 3 }; // initialize double variable with integer value 3 d = 6; // assign double variable the integer value 6 |
- When passing a value to a function where the function parameter is of a different data type:
1 2 3 4 5 |
void doSomething(long l) { } doSomething(3); // pass integer value 3 to a function expecting a long parameter |
- When returning a value from a function where the function return type is of a different data type:
1 2 3 4 |
float doSomething() { return 3.0; // Return double value 3.0 back to caller through float return type } |
- Using a binary operator with operands of different types:
1 |
double division{ 4.0 / 3 }; // division with a double and an integer |
In all of these cases (and quite a few others), C++ will use type conversion
to convert one data type to another data type.
There are two basic types of type conversion
: implicit type conversion
, where the compiler automatically transforms one data type into another, and explicit type conversion
, where the developer uses a casting operator to direct the conversion.
We’ll cover implicit type conversion
in this lesson, and explicit type conversion
in the next.
Implicit type conversion
Implicit type conversion (also called automatic type conversion or coercion) is performed whenever one data type is expected, but a different data type is supplied. If the compiler can figure out how to do the conversion between the two types, it will. If it doesn’t know how, then it will fail with a compile error.
All of the above examples are cases where implicit type conversion
will be used.
There are two basic types of implicit type conversion
: promotions
and conversions
.
Numeric promotion
Whenever a value from one fundamental data type is converted into a value of a larger fundamental data type from the same family, this is called a numeric promotion (or widening, though this term is usually reserved for integers). For example, an int
can be widened into a long
, or a float
promoted into a double
:
1 2 |
long l{ 64 }; // widen the integer 64 into a long double d{ 0.12f }; // promote the float 0.12 into a double |
While the term numeric promotion
covers any type of promotion, there are two other terms with specific meanings in C++:
- Integral promotion involves the conversion of integer types narrower than
int
(which includesbool
,char
,unsigned char
,signed char
,unsigned short
, andsigned short
) to anint
(if possible) or anunsigned int
(otherwise). - Floating point promotion involves the conversion of a
float
to adouble
.
Integral promotion
and floating point promotion
are used in specific cases to convert smaller data types to int
/unsigned int
or double
, because int
and double
are generally the most performant types to perform operations on.
The important thing to remember about promotions is that they are always safe, and no data loss will result.
For advanced readers
Under the hood, promotions generally involve extending the binary representation of a number (e.g. for integers, adding leading 0s).
Numeric conversions
When we convert a value from a larger type to a similar smaller type, or between different types, this is called a numeric conversion. For example:
1 2 |
double d{ 3 }; // convert integer 3 to a double (between different types) short s{ 2 }; // convert integer 2 to a short (from larger to smaller type within same type family) |
Unlike promotions, which are always safe, conversions may or may not result in a loss of data. Because of this, code that causes an implicit conversion to be performed will often cause the compiler to issue a warning.
The rules for conversions are complicated and numerous, so we’ll just cover the common cases here.
In all cases, converting a value into a type that doesn’t have a large enough range to support the value will lead to unexpected results. For example:
1 2 3 4 5 6 7 8 9 |
int main() { int i{ 30000 }; char c = i; // chars have range -128 to 127 std::cout << static_cast<int>(c); return 0; } |
In this example, we’ve assigned a large integer to a char (that has range -128 to 127). This causes the char to overflow, and produces an unexpected result:
48
However, converting from a larger integral or floating point type to a smaller similar type will generally work so long as the value fits in the range of the smaller type. For example:
1 2 3 4 5 6 7 |
int i{ 2 }; short s = i; // convert from int to short std::cout << s << '\n'; double d{ 0.1234 }; float f = d; std::cout << f << '\n'; |
This produces the expected result:
2 0.1234
In the case of floating point values, some rounding may occur due to a loss of precision in the smaller type. For example:
1 2 |
float f = 0.123456789; // double value 0.123456789 has 9 significant digits, but float can only support about 7 std::cout << std::setprecision(9) << f << '\n'; // std::setprecision defined in iomanip header |
In this case, we see a loss of precision because the float
can’t hold as much precision as a double
:
0.123456791
Converting from an integer to a floating point number generally works as long as the value fits within the range of the floating type. For example:
1 2 3 |
int i{ 10 }; float f = i; std::cout << f; |
This produces the expected result:
10
Converting from a floating point to an integer works as long as the value fits within the range of the integer, but any fractional values are lost. For example:
1 2 |
int i = 3.5; std::cout << i << '\n'; |
In this example, the fractional value (.5) is lost, leaving the following result:
3
Conversions that could cause loss of information, e.g. floating point to integer, are called narrowing conversions. Since information loss is generally undesirable, brace initialization doesn’t allow narrowing conversions.
1 2 |
double d{ 10.0 }; int i{ d }; // Error: A double can store values that don't fit into an int |
Evaluating arithmetic expressions
When evaluating expressions, the compiler breaks each expression down into individual subexpressions. The arithmetic operators require their operands to be of the same type. To ensure this, the compiler uses the following rules:
- If an operand is an integer that is narrower than an int, it undergoes integral promotion (as described above) to int or unsigned int.
- If the operands still do not match, then the compiler finds the highest priority operand and implicitly converts the other operand to match.
The priority of operands is as follows:
- long double (highest)
- double
- float
- unsigned long long
- long long
- unsigned long
- long
- unsigned int
- int (lowest)
We can see the usual arithmetic conversion take place via use of the typeid
operator (included in the <typeinfo> header), which can be used to show the resulting type of an expression.
In the following example, we add two shorts:
1 2 3 4 5 6 7 8 9 10 11 |
#include <iostream> #include <typeinfo> // for typeid() int main() { short a{ 4 }; short b{ 5 }; std::cout << typeid(a + b).name() << " " << a + b << '\n'; // show us the type of a + b return 0; } |
Because shorts
are integers, they undergo integral promotion
to ints
before being added. The result of adding two ints
is an int
, as you would expect:
int 9
Note: Your compiler may display something slightly different, as the format of typeid.name() is left up to the compiler.
Let’s take a look at another case:
1 2 3 4 5 6 7 8 9 10 11 |
#include <iostream> #include <typeinfo> // for typeid() int main() { double d{ 4.0 }; short s{ 2 }; std::cout << typeid(d + s).name() << ' ' << d + s << '\n'; // show us the type of d + s return 0; } |
In this case, the short
undergoes integral promotion
to an int
. However, the int
and double
still do not match. Since double
is higher on the hierarchy of types, the integer 2
gets converted to the double
2.0
, and the doubles
are added to produce a double
result.
double 6.0
This hierarchy can cause some interesting issues. For example, take a look at the following code:
1 |
std::cout << 5u - 10; // 5u means treat 5 as an unsigned integer |
you might expect the expression 5u - 10
to evaluate to -5 since 5 - 10 = -5. But here’s what actually happens:
4294967291
In this case, the signed integer
10
is promoted to an unsigned integer
(which has higher priority), and the expression is evaluated as an unsigned int
. Since -5
can’t be stored in an unsigned int
, the calculation wraps around, and we get an answer we don’t expect.
This is one of many good reasons to avoid unsigned integers
in general.
Quiz time
Question #1
What’s the difference between a numeric promotion and a numeric conversion?
![]() |
![]() |
![]() |
One common type casting that comes up all the time in codes I work with is switching between different types of pointers. The most unavoidable example is something like
Float *ptr = (Float *)smalloc(num_bytes);
Because smalloc() is like malloc in that it returns a void type. You might wonder why not use the new operator for dynamic allocation? The reason is that on the architecture we are using (e.g., BlueGene L/P/Q) we want to align the memory usage properly in order to avoid cache misses. So we write a function smalloc() that takes care of all that for us, which new would not do if we are using the GNU compiler (maybe IBM's compiler will do it automatically, I don't know). We can also write into it code to verify the allocation was successful and if not, report the class and function where the error occured,
Float *ptr=(Float *)smalloc(num_bytes,cname,fname);
Also Float is some typedef, probably float or double, depending on the precision we've decided to use.
Another use, which looks dangerous, but we do it anyways because we care more about performance than C++ doctrine, is something like:
int num_bytes = N*2*sizeof(Float);
Complex *c=(Complex *c)smalloc(num_bytes);
// next, code that fills in the array of complex numbers ...
Float *temp=(Float *)c;
// Now we want the norm of the complex vector, and we use an optimized function that say uses standard reduction algorithm to take advantage of multithreading on our 64-way processor, or maybe on a GPU accelerator:
Float my_norm=norm(temp,N*2);
By converting to Float array, the complex array looks just like an array of floats 2*N long and norm knows what to do with such an array. If norm is a library function that we a calling from some external package, it probably has no idea what to do with our abstract Complex type. So we trick it. Is it "dangerous"? Maybe, but so is driving a car.
Another trick may be if we have to call MPI_Send() and MPI_Recv() to send the array of Complex numbers between compute nodes. Then Float* typically means something to these functions, but Complex* does not. It would be ridiculous to copy the array elements into a float array just to communicate them when a simple type casting does the job (provided we do not use the C++ standard complex class, but rather our own class with just two data elements --- note that they also cannot be private, again for performance reasons --- we'd be friending practically every function in our code, which is plain silly).
Thanks for all these great examples and the helpful summary. Next time someone asks me why type safety matters I'm going to send them here.
"Interesting" is one way to describe it....
The following snippet of code explained in this chapter isn't working as described.
In the following program, the compiler will typically complain that converting a double to an int may result in loss of data:
int nValue = 100;
nValue = nValue / 2.5;
my code:
{
int ivalue = 100;
cout << "output" << ivalue/2.5 <<endl;
ivalue = ivalue / 2.5;
cout << "ivalue" <<ivalue << endl;
return 0;
}
Both the cout returns 40, no compiler warning or error. I am using codeblocks.
secondly, if the hierarchical nature of type conversion is true then why doesn't the following implicit cast of ivalue produce compiler error ?
int main()
{
int ivalue = 0;
double dvalue1 = 3.54567;
double dvalue2 = 2.34345;
ivalue = dvalue1 / dvalue2;
cout << "ivalue" <<ivalue << endl;
return 0;
}
I would like to point out that "int nValue = 10 * 2.7" does not really work "as expected". It is not possible to store the value 2.7 in a double. Instead, the value stored in the machine register is something like 2.7000000000000001776... (continuing to 51 digits). You are lucky that the IEEE rounding rules allow the computation to give 27 at the end, but in general you should never expect a floating-point computation on inexact values to give an exact result.
The value 2.5 can be represented exactly as a double, however. So you are guaranteed that 10 * 2.5 will be 25.
Hello, I have a question: Is it safe to use static_cast([float value here]) to truncate a float value? And no, I'm not talking about rounding.
fixed
Wait sorry about that, the "int" in the angled brackets after "static_cast" seems to disappear.
was using static_cast ever safe? :p
thank you zingmars
sry
link is
http://pastebin.mozilla.org/1253503
This is what compiler does too - it does whatever you ask from it, and assigns the results to a memory location, and then you store it in p1.
And no - the line does not work. Firstly because apparently you can't do static cast on pointers (they're memory locations, not values or types or anything).
Also - one does not simply convert one class to another. It doesn't work that way.
Just my 2 cents.
So I guess the answer to the 'what does it hold' is - nothing. It holds nothing because you can't do things this way.