format

6.02.2017

C++ Run down Series: variables, types and casting

 

Abstract 

This week we will discuss variables, types and casting between types. We'll discuss how C++ sees variables, and what are types for. Variables are named positions of memory used by the programmer to hold values, during the lifetime of a process. The positions may be constant, and thus hold the same value throughout the process lifetime or be modifiable (thus the name variable).

Variables


In the previous post we looked at the requirements to write C++ software on all the major systems, using a simple "Hello world" as a motivation.
This week, we'll look into what makes a variables, types and conversions.
Let us start with variables; variables are nothing but named positions on virtual memory. These names allow us to know what goes where while the compiler will deal with the actual position in memory.
In most languages the usage of a variable is transparent. Let us look at an assignment operation:
 
<variable> = <expression>;

The left side of a assignment operation must be a variable. This is both expected and reasonable: if we want to save the result of a expression's computation, we'll save it onto a variable.
In typed programming languages, which means that each variable has a associated type, before we can use a variable we must declare it.
 
int iResult;

iResult = 1+1; 

In the above example, iResult is a name which is declared to be a integer position. When the compiler encounters this line, it'll reserve a virtual memory position to hold a single integer.
The second line is an assignment. The compiler's syntax checker will look at the line and decide if it is well-formed instruction: if all the symbols are recognized, if the line ends in with a semi-colon, if the right side is a valid expression and if the left side is a known name. If any of these conditions fail, the compilation will fail.

Types


Types are named concepts for variables. They specify how the compiler encodes instructions to deal with the memory holding the variable.
There are several types already defined on the language. There are LiteralTypes, which consist on bool, char, int, float, long and double.
Types that are not LiteralTypes, like user-defined types also exist. The standard defines several, for instance std::string, std::fstream, or std::iterator.

A program may use all available types to define new variables. It is possible also for a programmer to specify her own types.

Every variable must have an associated type; this information is used by the compiler to check the correctness of the program: you'll be warned if you try to save a char in a int, and the program's compilation would halt if you tried to save a byte in a string.

However, other conversions are possible. A char can be saved into a byte. A integer can be narrowed to a char. And a long can hold a int.
By and large, all LiteralTypes can be converted into another LiteralType. Let us call the preexistant variable value's type a "predecessor type" and the destination type the "successor type".
Whenever a predecessor type is stored with less bytes than the successor type, there is no cause of worry. However, when the successor type is itself stored with less bytes than the predecessor, we have a narrowing conversion. Narrowing conversions may yield unexpected results and in extreme cases, these conversions may be the source of very strange behavior in runtime. The most common cases of such problems are the conversions of floating point types (float, double) to other literals of lesser storage (byte, int). The most dramatic situation involving a casting problem was the explosion of the Ariene 5, on the 4ยบ of July in '95. A 64 bit floating point variable was narrowingly cast to a 16 bit integer, which led the guidance system astray.

Conversions


The conversions between types may be implicit or explicit. Implicit conversions are those which the compiler deduces the intervening types. The deduction takes into account the predecessor and successor types, and the compiler does its best to create code that does the conversion. If the compiler does not know of a way to do the conversion, the compiler stops the compilation process yielding an error.
An implicit conversion:
float a = 0.5f;

double b = a;


Explicit conversions are more programmer involved. The programmer specifies a conversion to be done through a casting operation.
An explicit casting operation:
 
int a = 1;

float b = (float) a;

In these operations, the type is specified before the predecessor variable appears. The (float) operator tells the compiler that a should be converted into a temporary float value and that value should be used to alter the memory that b holds.


There are other operators to handle type conversion. These are: dynamic_cast, reinterpret_cast, static_cast, and const_cast.
The general syntax is
<variable> = [dynamic,reinterpret,static,const]_cast< sucessor_type >( predecessor_value );

ex:
const float a = const_cast< const float >( 1 );


Each of these conversion primitives are similar to the explicit conversion above, however they have advantages over the explicit method. Most of the advantages are related to C++ virtual polymorphism but there three operators still relevant with LiteralTypes: const_cast, static_cast and reinterpret_cast which we'll talk about in another post.

If you liked the post, have any questions or suggestions, feel free to comment.
Next post, we'll deal with program control flow through conditionals.