The importance of understanding the conversion of types
C++ is a spoken language Strictly typed, term usually translated as heavily typed, this means that the type of variables is always right and the operations must be defined for specific types. Generating an object of one type through an expression of another type involves a conversion, or casting.
As just explaining what each conversion does may not be very revealing, I added simple examples at the end.
There is the static_cast
, dynamic_cast
, const_cast
and reinterpret_cast
, what is the difference between these?
static_cast<novo_tipo>(expressão);
static_cast, static conversion
This is the most common conversion. It is said static because its validity is analyzed during compilation, the main possibilities are:
- Implicit conversion between types (such as
float
for int
).
- Call a builder of
novo_tipo
through the outcome of expressão
.
- Use user-defined conversion operator of the result of
expressão
to the novo_tipo
.
- Convert pointers between class hierarchy as long as the classes are not virtual. (
static_cast
does not check the validity of the conversion during execution.)
- Convert pointers
void*
for any other type of pointer. (undefined result if pointer alignment is not correct.)
dynamic_cast<novo_tipo>(expressão);
dynamic_cast, dynamic conversion
This conversion is special for references or pointers to polymorphic objects (classes containing virtual functions). It is called dynamic because it checks during the execution of the program if the conversion is valid when descending in the class hierarchy. In main:
- When converting pointers up in the hierarchy (
expressão
is derived from novo_tipo
), behaves as an implicit conversion.
- When converting pointers down in the hierarchy, (
expressão
is the basis of novo_tipo
), checks whether expressão
originally referred to a pointer to novo_tipo
and, if yes, returns the adjusted pointer. If the check fails, it returns nullptr
.
- Conversion between references is similar, but generates exception
std::bad_cast
in case of failure.
const_cast<novo_tipo>(expressão);
const_cast, constancy conversion
This conversion has the only function of adding or removing the property const
. Any conversion can generate a reference or pointer to an object so that it is treated as constant, but only const_cast
can generate a new reference or pointer to a constant object to be treated as modifiable (nonconstant). This conversion does not generate any instruction, it is only a directive for the compiler.
reinterpret_cast<novo_tipo>(expressão);
reinterpret_cast, conversion by reinterpretation
This conversion is that more distance from the characteristic heavily typed of the C++ language, as it commands the compiler to reinterpret the result of expressão
as if it were from novo_tipo
, in general without performing any operation or verification on the values being converted. Roughly, reinterpret_cast
is way of telling the compiler: "Trust me, these numbers I’m giving you are what I say they are" . Its main uses are:
- Convert pointer or reference to any type of object to pointer or reference to any other type of object.
- Convert a pointer to an integer number.
- Convert an integer number to pointer.
When to use each?
Try to use the conversion that best expresses your intentions. As only the definition of each type of conversion may not be very elucidative, follow examples of each:
To static_cast
is the most common and usually occurs implicitly in an implicit conversion, as in the example:
int i = 5;
float x = i; //conversão implícita
float y = static_cast<float>(i); //conversão explícita
Implicit conversions make it easy to handle numeric and local variables, but it is always recommended to make the conversion explicit when exporting the variable to some function. In the example below, the result with and without the cast
is the same (soon after I will point out a possible problem):
int f(int x)
{
return x*2;
}
int main()
{
float x = 1.f;
std::cout << "Sem cast : " << f(x) << std::endl;
std::cout << "Com cast : " << f(static_cast<int>(x)) << std::endl;
}
Upshot:
Sem cast : 2
Com cast : 2
Now, say a new function is introduced without changing the existing parts of the previous code:
int f(double x)
{
return x*5;
}
The result of the program changes:
Sem cast : 5
Com cast : 2
The reason for this is that the implied conversion of float
for double
is preferred over the implicit conversion of float
for int
.
Although required in some cases (such as converting pointers void*
for other types) static_cast
has a function that depends more on good organization and maintenance of the code.
dynamic_cast
is the way to check at runtime whether the type of polymorphic object passed is of a certain type (for this the program makes use of RTTI, I will omit details). In a simplified example, imagine a base class with two derivative types:
struct Base{
virtual ~Base(){};
};
struct DerivadaA : public Base
{};
struct DerivadaB : public Base
{};
Let’s say we have a function that must deal with objects of the type Base
,
void f(Base* ponteiro_base)
But at some point the execution must be different if the object passed is DerivadaA
or DerivadaB
. Like dynamic_cast
returns nullptr
in case of failure, we can do the following:
void f(Base* ponteiro_base)
{
//tenta cast para ponteiro do tipo DerivadaA
DerivadaA * objA = dynamic_cast<DerivadaA*>(ponteiro_base);
if(objA != nullptr)
{
std::cout<<"Objeto do tipo A" << std::endl;
return;
}
//tenta cast para ponteiro do tipo DerivadaB
DerivadaB * objB = dynamic_cast<DerivadaB*>(ponteiro_base);
if(objB != nullptr)
{
std::cout<<"Objeto do tipo B" << std::endl;
return;
}
}
Click here to see the example above in action.
Remove the property const
of an object is rare need, and modify a declared object const
results in undefined program behavior, so it is up to the programmer to use this conversion cohesively.
An illustrative example: When one wishes to return a reference to a member of a class (the famous setters
and getters
):
class ClasseX
{
int X;
public:
//retorna referência a membro da classe
int& getRefX()
{
return X;
};
};
Since the function does not change the class state, I may want to mark it const
:
int& getRefX() const
But hence the code does not compile (the signature const
of the function makes its members const
within this)...
error: binding 'const int' to reference of type 'int&' discards qualifiers
const_cast
can be used to return the non-constant reference:
//retorna referência a membro da classe
int& getRefX()
{
//(algum comentário explicando o const_cast)
return const_cast<int&>(X);
};
const_cast
should be used very carefully, as changing the value of a originally constant variable makes the program poorly formed (Undefined behaviour). In the example above, if any object of the class ClasseX
was originally constant, the function would still work in this, but the value of X
should not be modified by the reference returned.
The reinterpret_cast
moves away from the notion of objects and approaches the notion of bits (information).
For example, let’s say you’re programming a microcontroller and the manual says that the sound card reads the information from the address 33
in memory:
//endereço obtido do manual do microchip
static const int ADDR_PLACA_DE_SOM = 33;
To write information in this area of memory, you need to convert this position to a pointer, with reinterpret_cast
you can do it:
//cria ponteiro para inteiros na posição dita pelo manual
int* som_pt = reinterpret_cast<int*>(ADDR_PLACA_DE_SOM);
Note that the conversion is quite free, you can create a pointer to any type of data:
//cria ponteiro para chars na posição dita pelo manual
char* som_pt = reinterpret_cast<char*>(ADDR_PLACA_DE_SOM);
C++ also supports cast in the style of the C language, as it is interpreted by the compiler?
As C-style conversions are of the type:
(novo_tipo)(expressão)
An example:
//gera um inteiro através do float 3.14
(int)(3.14f)
This type of conversion is present in C++ mainly for compatibility with the C language.
The compiler attempts the following order of conversions when a cast in C-style is used:
const_cast
.
static_cast
, ignoring restricted access if used in class hierarchies.
static_cast
followed by const_cast
reinterpret_cast
reinterpret_cast
followed by const_cast
That is, the compiler tries almost everything to generate a new type object, including removing const
. This type of conversion can generate unwanted conversions (up to the not object-oriented reinterpret_cast
!) that hide bugs that can be propagated by the logic of the program.
This answer is a simplification. I reiterate that to understand well the conversion of objects indicates a good understanding of the object orientation paradigm. The main references were:
cppreference (in English): static_cast, dynamic_cast, const_cast, reinterpret_cast, Explicit type Conversion