Introduction to type conversion
The value of an object is stored as a sequence of bits, and the data type of the object 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 might be stored as binary 0000 0000 0000 0000 0000 0000 0000 0011
, whereas floating point value 3.0 might be stored as binary 0100 0000 0100 0000 0000 0000 0000 0000
.
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 the memory allocated for float
variable f
. Instead, it needs to convert the integer value 3
to the equivalent floating point number, which can then be stored in the memory allocated for f
.
The process of converting a value from one data type to another data type is called a type conversion.
Type conversion can be invoked in one of two ways: either implicitly (as needed by the compiler), or explicitly (when requested by the programmer). We’ll cover implicit type conversion in this lesson, and explicit type conversions (casting) in upcoming lesson 8.5 -- Explicit type conversion (casting).
Implicit type conversion
Implicit type conversion (also called automatic type conversion or coercion) is performed automatically by the compiler when one data type is required, but a different data type is supplied. The vast majority of type conversions in C++ are implicit type conversions. For example, implicit type conversion happens in all of the following cases:
When initializing (or assigning a value to) a variable with a value of a different data type:
1 2 |
double d{ 3 }; // int value 3 implicitly converted to type double d = 6; // int value 6 implicitly converted to type double |
When the type of a return value is different from the function’s declared return type:
1 2 3 4 |
float doSomething() { return 3.0; // double value 3.0 implicitly converted to type float } |
When using certain binary operators with operands of different types:
1 |
double division{ 4.0 / 3 }; // int value 3 implicitly converted to type double |
When using a non-Boolean value in an if-statement:
1 2 3 |
if (5) // int value 5 implicitly converted to type bool { } |
When an argument passed to a function is a different type than the function parameter:
1 2 3 4 5 |
void doSomething(long l) { } doSomething(3); // int value 3 implicitly converted to type long |
What happens when a type conversion is invoked
When a type conversion is invoked (whether implicitly or explicitly), the compiler will determine whether it can convert the value from the current type to the desired type. If a valid conversion can be found, then the compiler will produce a new value of the desired type. Note that type conversions don’t change the value or type of the value or object being converted.
If the compiler can’t find an acceptable conversion, then the compile will fail with a compile error. Type conversions can fail for any number of reasons. For example, the compiler might not know how to convert a value between the original type and the desired type. In other cases, statements may disallow certain types of conversions. For example:
1 |
int x { 3.5 }; // brace-initialization disallows conversions that result in data loss |
Even though the compiler knows how to convert a double
value to an int
value, such conversions are disallowed when using brace-initialization.
There are also cases where the compiler may not be able to figure out which of several possible type conversions is unambiguously the best one to use. We’ll see examples of this in lesson 8.11 -- Overloaded function resolution.
So how does the compiler actually determine whether it can convert a value from one type to another?
The standard conversions
The C++ language standard defines how different fundamental types (and in some cases, compound types) can be converted to other types. These conversion rules are called the standard conversions.
The standard conversions can be broadly divided into 4 categories, each covering different types of conversions:
- Numeric promotions (covered in lesson 8.2 -- Floating-point and integral promotion)
- Numeric conversions (covered in lesson 8.3 -- Numeric conversions)
- Arithmetic conversions (covered in lesson 8.4 -- Arithmetic conversions)
- Other conversions (which includes various pointer and reference conversions)
When a type conversion is needed, the compiler will see if there are standard conversions that it can use to convert the value to the desired type. The compiler may apply zero, one, or more than one standard conversions in the conversion process.
As an aside...
How do you have a type conversion with zero conversions? As an example, on architectures where int
and long
both have the same size and range, the same sequence of bits is used to represent values of both types. Therefore, no actual conversion is needed to convert a value between those types -- the value can simply be copied.
The full set of rules describing how type conversions work is both lengthy and complicated, and for the most part, type conversion “just works”. In the next set of lessons, we’ll cover the most important things you need to know about type conversions. If finer detail is required for some uncommon case, the full rules are detailed in technical reference documentation for implicit conversions.
Let’s get to it!
![]() |
![]() |
![]() |
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.