What is widening , truncation , promotion , and conversion between , the different C , arithmetic data types ?

 
The arithmetic data types in C , are the integer types , such as int or unsigned long , and the floating point types , such as float , or long double .

Conversion of the integer types

What is widening ?

Widening only applies to the signed and unsigned integer types . It does not apply to other types , such as float or double .

Widening is not about converting from signed to unsigned , or from unsigned to signed , it is about expanding the signedness of an integer type , from a smaller type to a larger type , so from a smaller number of bits , to a larger one . The signedness of the integer type does not change .

For the signed type , widening is done by what is called : sign extension . If the value of the signed type is negative , it is extended by filling the new bits by the value 1 , if the value of the signed type is nonnegative , than it is extended by filling the newly allocated bits , with 0 .

For the unsigned type , the newly allocated bits are filled with 0 .

What is truncation ?

Truncation happens only for the integer types , that have the same signedness , when passing from a larger integer type , to a smaller integer type , such as , when passing from int to char , or from unsigned int to unsigned char.

Be it is signed or unsigned , the larger type is made to fit the smaller type , by discarding the bits , of the larger type , that lies outside of the width of the smaller type , keeping only the lower order bits .

Truncation as described , which is keeping only a limited number of bits , doesn’t happen when converting from a floating point type , to an integer type , or between floating point types .
When converting from a floating type , to an integer type , the fractional part is discarded , the floating type bits are not made to fit the integer type width , but the floating type is transformed from a floating point representation , to an integer representation .

The integer conversion procedure

Conversion of the integer types , consists of either performing widening or truncation first , and later reinterpreting the bits .
So first truncation or widening is performed . Truncation or widening does not change the signedness of the type , the type is only made to fit , a larger or narrower width , of the same signedness .
After having performed widening or truncation , the gotten bits in the target width , are only reinterpreted , as belonging to a new integer type , the target type .

The only exception to this rule , is when converting to the _Bool type , any nonzero value is converted to 1 , and any zero value , is converted to 0 .

Having explained , how conversion happens for the integer types , let us explain the concept of integer rank , before explaining when conversion to another integer type , takes place .

What is a rank ?

Each integer type in C has a rank .

unsigned , and signed integer types of the same type , disregarding the signedness , have the same rank . For example int , and unsigned int , have the same rank .

The order of the ranks of the integer types is , as follow :

_Bool < signed char < short < int < long < long long 

The rank of any standard integer type , outlined above , is larger than the rank of any implementation defined , extended integer type , having the same width .

The char , and signed char integer types , have the same rank .

The rank of an enum type is equal to its assigned implementation , defined integer type .

Integer promotion

Integer types , which have a rank smaller than int , each is promoted , to either the int type , if the int type can represent all its possible values , or to the unsigned int type , in the other case .

Integer types having a rank smaller than int , are promoted when an operator expects , that one of its operand , to be of an arithmetic type . Arithmetic types in C , are the integer or floating point types . An example of such an operator , is the subtraction operator - , or the unary negation operator - .

Integer promotion , is not how the integer is converted to another type , it is when a integer is converted to another type , in this case it is because it has a lower rank than int .

unsigned char x = 1;
signed char y = -x;
/* 
x , is an unsigned char type , 
  it has a rank lower than int .
The unary negation operator is 
  used , as such x must be promoted . 
An int on 32 bits systems , have a 
  typical width of 32 bits , as such 
  it can store all the possible values
  of an unsigned char , which is limited
  to only 8 bits . As such the target
  promotion type is an int .
Now that the target type is decided , 
  which is an int , it has a larger
  width than an unsigned char , as
  such widening must be performed . 
Widening does not change  signedness ,
  so the unsigned char is widened to an
  unsigned int , by using zero fill . 
The resulting value is 1 ,  and has a bit 
  representation of : 
  00000000000000000000000000000001
The resulting bits , are interpreted as if , 
   as being of the type signed int , and the
   negation operator is applied .
The result of the negation operator is -1 , 
   which , has a bit representation of :
    11111111111111111111111111111111
The value is to be stored in a signed 
  char type , both int and signed char ,
  have the same signedness , as such 
  truncation is applied .
The width of a signed char type is 
  8 bits , as such the leading 24 bits 
  are discarded , and the result is : 
  11111111 , which is equal to -1 . */

Function call or return value

When making a function call , and an argument is of an integer type , different from the target parameter integer type , type conversion occurs .

The argument is converted to the parameter type , by first truncation , or widening , to the same width , and later on reinterpreting the bits as of the target parameter type .

#include<stdio.h>

void trivialFunction( unsigned int val){
  if( val == 4294967295){
    printf( "%u is equal to 4294967295\n" , val);
  }
  else{
    printf( "%u is different from 4294967295\n" , val);}}


int main( void){
  signed char x	= -1 ;	
  trivialFunction( x);
  unsigned char y = x ;
  trivialFunction( y);}

/* Output
4294967295 is equal to 4294967295
255 is different from 4294967295
*/

/*
In the first call to trivialFunction , 
  the passed argument is of type signed 
  char . trivialFunction expects its 
  argument , to be of the type unsigned 
  int , as such the argument must be 
  converted .
First widening to the same width of 
  unsigned int takes place . 
x is a signed char , as such it is 
  widened to a signed int , by sign
  extension . The value of x is -1 , 
  and it has a bit representation of 
  11111111 . The resulting value is -1 ,
  and it has a bit representation of 
  11111111111111111111111111111111 .
This bit pattern , is next reinterpreted
  as an unsigned int , so
  11111111111111111111111111111111 ,  
  as an unsigned int , has a value of 
  4294967295 . This is why trivialFunction
  prints : 4294967295 is equal to 4294967295


The variable y , has a type of unsigned 
  char . It is assigned the value of x , 
  which is a signed char .
Both x , and y have the same width , as such 
  no widening occurs , only the bits of x ,
  are reinterpreted as being unsigned .
The bits of x , has a value of -1 , which is 
  11111111 , when reinterpreted as unsigned ,
  this yield the value of 255 . 
Next trivialFunction is called with y , 
  as a parameter . 
Widening occurs  , because y is an unsigned 
  char , and the function parameter is an 
  unsigned int . It is done by using 
  zero fill . 
Hence the value of 1 which has a bit 
  representation of 11111111 , is widened 
  to the value 255 which has a bit 
  representation of : 
  00000000000000000000000011111111 
The function prints : 
  255 is different from 4294967295 
*/

When the return value of a function is different from its return type , the return value is converted to the function’s return type .

Assignment and initialization

When performing assignment or initialization to an integer variable , using the = operator , and the expression to be assigned , is of a different integer type , integer conversion takes place .

unsigned char x = 1 ; 
/*
1 is an integral literal ,  it is 
  of the type int . 
The type int has a typical width 
  of 32 bits , while an unsigned 
  char has a typical width 
  of 8 bits .
The bits of the int type 
  00000000000000000000000000000001
  are reinterpreted as  being the 
  bits of an unsigned int type . 
  The result is :
  00000000000000000000000000000001 
The unsigned int value is truncated to 
  8 bits , and the value gotten is 
  00000001 , which is assigned to 
  the unsigned char x .*/

For further information , about the type of integer literals , you can check this article .

Arithmetic operators

When one of the following operators , is being used , an integer conversion will occur .

* / % + -  
/* 
Multiplication , division , 
  modulos , addition , 
 subtraction  .*/


< <= > >= == !=  
/*
 Relational less , less or equal , 
  larger , larger or equal , 
  equal , not equal .*/


& ^ | 
/* 
Bitwise and , xor , or .*/


?: 
/* 
The Ternary operator expression , 
  must return a value of a specific 
  type , the second and third operand , 
  are converted to a same type . */ 


 +=  -=  *=  /=  %=  &=   ^=  |=  
/* operate and assign operators  */

In the case of these operators , a common type for the operands and the result must be determined . This is done as specified by the following table .

UnsignedSignedRankOperands and Result type
uTsTuT >= sTuT
uTsTuT < sTsT , if sT is capable of holding , all the possible values of : uT .

The unsigned type of sT , if sT is not capable of holding all the possible values of : uT .
uT , means an unsigned type , such as : unsigned int
sT , means a signed type , such as :int
The result of relational operators such as < , is always 0 for true , and 1 for false , and it is always of the type int .
int si = -1 ; 
unsigned int ui = 0 ; 

ui = ui + si ; 
/*
int , and unsigned int , have the same 
  rank , one is signed , and the other 
  is unsigned , as such both operands , 
  must be converted to the unsigned int 
  type , and the result of the operation ,
  is of an the unsigned int type . 
ui is unsigned , so no conversion is 
  necessary . 
To convert si , to the unsigned int type , 
  and since both si and ui have the same 
  width , the bits of si , which are 
  11111111111111111111111111111111 ,
  are kept as is , they are only reinterpreted
  as belonging to a signed type , so now
  they have a value of 4294967295 . 
The addition is performed , and the result 
  of the addition operation , is as such :
  4294967295 + 0 = 4294967295 , and is
  of the type unsigned int . 
ui is of type unsigned int , as such 
  no conversion is necessary , and the 
  result of 4294967295 is stored in ui .
*/


long long lli = -1 ; 
ui = lli + ui ; 
/*
lli is of the type long long , 
  it has a higher rank than 
  unsigned int . long long 
  can hold all the values of 
  unsigned int , as such , 
  both operands must be of
  the type long long . 
lli is of the type long long , 
  so no conversion is necessary . 
ui is an unsigned int , it has 
  a bit representation of 
  11111111111111111111111111111111
  it is first extended to unsigned 
  long by using zero fill . 
  0000000000000000000000000000000011111111111111111111111111111111
  After that , the gotten bits are 
  reinterpreted as being of type 
  long long . The gotten value , is 
  the same as the value of ui , which 
  is 4294967295 .  
The addition is  performed between , 
  the two long long integer types , and 
  the result is -1 + 4294967295 , which
  is equal to 4294967294 , and is of 
  the type long long .
ui is of the type unsigned int , the result
  which is of the type long long  must 
  be converted to unsigned int  .
It is first converted to unsigned long long , 
  the bits pattern does not change ,  so it remains 
  0000000000000000000000000000000011111111111111111111111111111110 .
  Now that is is of the type unsigned long long , 
  it is truncated to the type of ui , which is 
  unsigned int , and it has the format : 
  11111111111111111111111111111110  , 
  and a value of : 4294967294 .
*/


long int li  = -1 ; 
li = li + ui ;
/*
ui is an unsigned int , whereas li
  is a long int . A long int has 
  a higher rank than an unsigned 
  int . Assuming that on this machine , 
  both long int and unsigned int have
  a width of 32 bits , this means 
  that long int is not capable of 
  representing all the possible values 
  of the unsigned int type , as such 
  the operands , and the result 
  must be of the unsigned long type . 
li bits are kept as is , and only
  reinterpreted as being of the 
  type unsigned long , as such li 
  will have the value of 4294967295 .
ui is converted to unsigned long , by 
  widening. unsigned int , and unsigned long 
  have the same width , as such the bits
  of ui remains the same , 
  11111111111111111111111111111110 .
Adding 4294967295 + 4294967294 = 
  8589934589 . The result is of 
  the type unsigned long , it cannot
  fit the width of the unsigned 
  long type , which has a max 
  value of 4294967295 , as such 
  overflow has occurred . 
  The modulo of the result , with
  regards to 2 to the power of 
  the number of bits ,
  of the unsigned long type is taken . 
  This is equal to 
  8589934589 % 4294967296 = 
    4294967293 , which is 
  11111111111111111111111111111101 
The result must be stored in li , 
  li is of the long type , the gotten
  value is of the type unsigned long , 
  the bits are kept as is , and the 
  result is only reinterpreted as being 
  a signed long , as such the value 
  of li is -3 . 
*/

For information on overflow of the signed , and unsigned integer types , you can check this , and this article .

Cast operator

Explicit conversion happens , when using the casting operator : (Type) expression . As an example , explicitly casting the int literal 1 , to the long type .

(long) 1

When using the cast operator , this is called explicit casting , all other cases of conversion , are called implicit casting .

Conversion of the floating point types

Converting from one floating type , to another

Each floating point type , has a different range , and precision . As such , when passing from floating point types , with higher precision and range , to floating point types with smaller precision and range , a loss of precision , an underflow , or an overflow , can occur .

double dbl = 16777217 ;
float fl = dbl ; 
/*
Loss of precision , fl value is
  16777216 .*/


dbl = 1.2e-50 ; 
fl = dbl ; 
/*
Underflow occurs , the behavior 
  is implementation defined , 
  in this case , fl has a value 
  of 0 .*/

dbl = 3.4e100;
fl = dbl ;
/*
Overflow occurs , the behavior 
  is implementation defined , 
  fl has a value 
  of positive infinity */

When passing from a floating point type , with a smaller range and precision , to a floating point type , with a higher range and precision , no precision loss , underflow , or overflow , occurs . The precision and range are preserved .

float fl = 1.099511627776e12f;
double dbl = fl ; 
/*
precision and range are 
  preserved , dbl has a 
  value of :
  1.099511627776e12f , 
  which is 2 to the 
  power 40 .*/

Converting from a floating point type , to an integer type

The process of converting a floating point type , to an integer type , can be thought of , as if , the number is first converted to the decimal notation , then the fractional part is discarded , then it is represented in its signed or unsigned representation .

double dbl = 1.3f; 
unsigned char uc = (unsigned char) x ;  
// uc is equal to 1 

dbl  = -1.3f;
uc = dbl  ; 
// uc is equal to 255

If the floating point number , is too large to be represented in an integer type , the behavior is not defined by the C standard , it is defined by the implementation .

double dbl = 3.4E30 ;

unsigned long ul  = dbl ; //ul is equal to 0
unsigned int ui  = dbl ; //ui is equal to 0
unsigned char uc  = dbl ; //uc is equal to 0

long li  = dbl ; //li is equal to -9223372036854775808
int si  = dbl ; //si is equal to -2147483648
signed char sc  = dbl ; //sc is equal to 0

Converting from an integer type , to a floating point type

When converting an integer type , to a floating point type , the integer value can always be represented , but there might be a loss of precision .

int si = 16777216 ; 
float fl = si ;  // fl is equal to 16777216

si = 16777217 ; 
float fl = si ; // fl is equal to 16777216

When does the conversion occurs ?

When a floating point type is involved , conversion occurs , when a function call is made , and the passed argument is of a different type than the function parameter , or when the return value of a function , is of different type , than its return type .

It also happens , when performing floating point variables , assignment and initialization . Finally , it occurs when using arithmetic operators , or when done explicitly , using explicit casting , such as (float) 1.0 .

Rank of floating point types

The rank of a floating point type , is always higher than the rank of an integer type , so an integer type , is always converted to a floating point type .

As for the floating point types , they have the following ranks :

float < double < long double 

If an arithmetic operation , involves two floating point types of different ranks , the floating point type with the lower rank , is converted to the floating point type with the higher rank .

The type of the result of an operation involving a floating point type , is the same type , as the one determined by the ranking algorithm .

Floating point types promotion

As with the integer type promotion , when performing arithmetic operations , a floating point type can be promoted , to a type with a higher precision and range .

This is not to be confused with how conversion happens , or in which rank , an arithmetic operation involving a floating point type , is to be performed , It is always performed using the higher operand rank .

The promotion rule , is defined in the macro FLT_EVAL_METHOD , defined in the header float.h .

If FLT_EVAL_METHOD is set to 0 , then arithmetic operations are done in the type of the widest operand , so if both operands are float , the arithmetic operation is done using the float type , so no promotion occurs .

If FLT_EVAL_METHOD is set to 1 , then arithmetic operations are performed , by promoting the operands to long double , if any operand is of the long double type , otherwise operands are promoted to the double type , even if both operands are of the float type .

If FLT_EVAL_METHOD is set to 2 , then arithmetic operations are performed , by promoting the operands , to the long double type .

If FLT_EVAL_METHOD is set to -1 , then the behavior is not defined .