I understand that reinterpret_cast is dangerous, I'm just doing this to test it. I have the following code:
int x = 0;
double y = reinterpret_cast<double>(x);
When I try to compile the program, it gives me an error saying
invalid cast from type 'float' to type 'double
What's going on? I thought reinterpret_cast was the rogue cast that you could use to convert apples to submarines, why won't this simple cast compile?
In C++ reinterpret_cast can only perform a specific set of conversions, explicitly listed in the language specification. In short, reinterpret_cast can only perform pointer-to-pointer conversions and reference-to-reference conversions (plus pointer-to-integer and integer-to-pointer conversions). This is consistent with the intent expressed in the very name of the cast: it is intended to be used for pointer/reference reinterpretation.
What you are trying to do is not reinterpretation. If you want to reinterpret an int as a double you'd have to convert it to a reference type
double y = reinterpret_cast<double&>(x);
although the equivalent pointer-based reinterpretation is probably more explicit
double y = *reinterpret_cast<double*>(&x); // same as above
Note though, that while reinterpret_cast can convert the reference/pointer types, the actual attempt to read the data through the resultant reference/pointer produces undefined behavior.
And in any case this, of course, can't make much sense on a platform with int and double of different size (since in case of larger double you will read beyond the memory occupied by x).
So, in the end it all boils down to what you were trying to achieve. Memory reinterpretation? See above. Some kind of more meaningful int to double conversion? If so, reinterpret_cast won't help you here.
Perhaps a better way of thinking of reinterpret_cast is the rouge operator that can "convert" pointers to apples as pointers to submarines.
By assigning y to the value returned by the cast you're not really casting the value x, you're converting it. That is, y doesn't point to x and pretend that it points to a float. Conversion constructs a new value of type float and assigns it the value from x. There are several ways to do this conversion in C++, among them:
int main()
{
int x = 42;
float f = static_cast<float>(x);
float f2 = (float)x;
float f3 = float(x);
float f4 = x;
return 0;
}
The only real difference being the last one (an implicit conversion) will generate a compiler diagnostic on higher warning levels. But they all do functionally the same thing -- and in many case actually the same thing, as in the same machine code.
Now if you really do want to pretend that x is a float, then you really do want to cast x, by doing this:
#include <iostream>
using namespace std;
int main()
{
int x = 42;
float* pf = reinterpret_cast<float*>(&x);
(*pf)++;
cout << *pf;
return 0;
}
You can see how dangerous this is. In fact, the output when I run this on my machine is 1, which is decidedly not 42+1.
If you are trying to convert the bits of your int to a the representation of a double, you need to cast the address not the value. You must also make sure the sizes match:
uint64_t x = 0x4045000000000000;
double y = *reinterpret_cast<double *>(&x);
reinterpret_cast is not a general cast. According to the C++03 spec section 5.2.10.1:
Conversions that can be performed explicitly using reinterpret_cast are listed below. No other conversion can be performed explicitly using reinterpret_cast.
And there is nothing listed that describes converting between integral and floating point types (or between integral types, even this is illegal reinterpret_cast<long>(int(3));)
The compiler rejects what you wrote as nonsense because int and double may be objects with different sizes. You could achieve the same effect this way, although it is certainly dangerous:
int x = 0;
double y = *reinterpret_cast<double*>(&x);
This is potentially dangerous because if x and y are diffrent sizes (let's say int is four bytes and double is eight bytes) then when you dereference the eight bytes of memory at &x to fill in y you will access four bytes of x and four bytes of ... whatever comes next in memory (possibly the start of y, or garbage, or something else entirely.)
If you want to convert a integer to a double, use a static_cast and it will perform conversion.
If you want to access the bit-pattern of x, cast to some convenient pointer type (say, byte*) and access up to sizeof(int) / sizeof(byte):
byte* p = reinterpret_cast<byte*>(&x);
for (size_t i = 0; i < sizeof(int); i++) {
// do something with p[i]
}
Reinterpret cast allows you to reinterpret a block of memory as a different type. This has to be performed on pointers or references:
int x = 1;
float & f = reinterpret_cast<float&>(x);
assert( static_cast<float>(x) != f ); // !!
The other thing is that it is in fact a quite dangerous cast, not only due to strange values coming out as results, or the assert above not failing, but because if the types are of different sizes, and you reinterpret from 'source' to 'destination' types, any operation on the reinterpreted reference/pointer will access sizeof(destination) bytes. If sizeof(destination)>sizeof(source) then that will step beyond the actual variable memory, potentially killing your application or overwritting other variables other than the source or destination:
struct test {
int x;
int y;
};
test t = { 10, 20 };
double & d = reinterpret_cast<double&>( t.x );
d = 1.0/3.0;
assert( t.x != 10 ); // most probably at least.
assert( t.y != 20 );
The reinterpret approach led me down a strange path with inconsistent results. In the end I found it much better to memcpy like this!
double source = 0.0;
uint64_t dest;
memcpy(&dest, &source, sizeof dest);
reinterpret_cast is best used for pointers. So a pointer to one object can be turned into a "submarine".
From msdn:
The reinterpret_cast operator can be
used for conversions such as char* to
int*, or One_class* to
Unrelated_class*, which are inherently
unsafe.
The result of a reinterpret_cast
cannot safely be used for anything
other than being cast back to its
original type. Other uses are, at
best, nonportable.
That's interesting. Maybe it's doing an implicit conversion from int to float before it attempts the cast to double. int and float types tend to be the same size in bytes (depending on your system of course).
Casting an int to a double doesn't require a cast. The compiler will perform the assignment implicitly.
The reinterpret_cast is used with pointers and references, e.g., casting an int * to a double *.
Use a union. It is the least error-prone way to memory map between an integer and a floating point type. Reinterpreting a pointer will cause aliasing warnings.
#include <stdio.h>
#include <stdint.h>
int main(int argc, char *argv[])
{
union { uint32_t i; float f; } v; // avoid aliasing rules trouble
v.i = 42;
printf("int 42 is float %f\n", v.f);
v.f = 42.0;
printf("float 42 is int 0x%08x\n", v.i);
}
Related
This question already has answers here:
Why cast a pointer to a float into a pointer to a long, then dereference?
(5 answers)
Closed 8 months ago.
Since my Numerical Analysis course exam is near, I was searching for a implementation code to to represent floating point numbers in C/C++? Then I found a line from one the codes in github. Can you please tell me, what is the meaning of the second line in the code snippet below, and how and why this is important?
float x = ...;
unsigned u = *(unsigned*)&x;
unsigned is just short for unsigned int and using C++-style casts the line would translate to
unsigned int u = *reinterpret_cast<unsigned int*>(&x);
However read below why this causes undefined behavior in either case.
(I recommend to not use C-style casts as in the line shown in the question, since it is not obvious to which C++-style cast they resolve.)
If x is a float variable, then the line is trying to reinterpret the object representation of the float variable as the object representation of an unsigned int, basically to reinterpret the float's memory as the memory of an unsigned int, and then stores the unsigned int value corresponding to that representation in u.
Step for step, &x is a pointer to x of type float*, reinterpret_cast<unsigned int*>(&x) is a pointer to x, but now of type unsigned int*. And then *reinterpret_cast<unsigned int*>(&x) is supposed to dereference that unsigned int* pointer to the float variable to retrieve an unsigned int value from the pointed-to memory location as if the bytes stored there represented an unsigned int value instead of a float value. Finally unsigned int u = is supposed to use that value to initialize u with it.
That causes undefined behavior because it is an aliasing violation to access a float object through a unsigned int* pointer. Some compilers have options which can be enabled to allow this (under the assumption that float and unsigned int have compatible size and alignment), but it is not permitted by the standard C++ language itself.
Generally, whenever you see reinterpret_cast (or a C-style cast that might resolve to a reinterpret_cast), you are likely to cause undefined behavior if you don't know exactly what you are doing.
Since C++20 the correct way to do this without undefined behavior is using std::bit_cast:
float x = /*...*/;
auto u = std::bit_cast<unsigned>(x);
or before C++20 using std::memcpy:
float x = /*...*/;
unsigned u;
static_assert(sizeof(u) == sizeof(x));
std::memcpy(&u, &x, sizeof(u));
The size verification is done by std::bit_cast automatically. Even without C++20 it would probably be a good idea to wrap the static_assert and memcpy in a similar generic function for reuse.
Both of these still require that the representation of x is also a valid representation for a u. Otherwise the behavior is still undefined. I don't know whether there even is any C++ implementation where this doesn't hold for all values in the float -> unsigned case.
Also as an additional note: C is a different language. The rules may well be different in C. For example there is obviously no reinterpret_cast in C to which the (unsigned*) cast could resolve and the object model is very different. In this case though, C's aliasing rules will have an equivalent effect.
It is not valid C++. The behavior (of the program) is undefined.
The cast expression would cause the alignment requirement to be violated (aka "strict aliasing violation").
See: §6.7 Memory and objects, and §6.8 Types of ISO/IEC JTC1 SC22 WG21.
The problem is the explicit cast will become a reinterpret_cast:
float boat = 420.69f;
// unsigned dont_do_this = * reinterpret_cast<unsigned *> (&boat);
// ~~~~~~~~~~~~~~~^
// Dereferencing `unsigned*` pointer which doesn't point to an `unsigned`.
float* is not Pointer-Interconvertible with unsigned*.
You could do this, instead:
auto since_cpp20 = std::bit_cast<unsigned>(boat); // include <bit>
// Alternatively:
unsigned since_c;
std::memcpy(&since_c, &boat, sizeof since_c);
Assuming that x is float, then the code is a hacky way to access the binary format of a float by copying its bits directly to an integer, i.e. without doing the normal floating point to integer conversion that would happen if you just wrote u = x;
float f = 12.5;
unsigned int _f = *reinterpret_cast<int*>(&f);
std::cout << _f << std::endl; // result: 1095237632
Can some explain me how such casting works? And what is represented by _f?
EDIT
So this number I got 1095237632 after converting to binary is 0b1000001010010000000000000000000 and this binary number is 12.5 in IEEE-754. Do I get it right?
Let's look at two functions. One of them casts float to int it regularly, and one of them reinterpret casts it using reinterpret_cast:
int cast_to_int(float f) {
// Rounds f to an int, rounding towards 0
return (int)f;
}
int reinterpret_cast_to_int(float i) {
// Just copies the bits
return *reinterpret_cast<int*>(&i);
}
So what actually happens? Let's look at the assembly:
cast_to_int(float):
cvttss2si eax, xmm0 // We cast to an int
ret
reinterpret_cast_to_int(float):
movd eax, xmm0 // We directly copy the bits
ret
In the first case, there's an assembly instruction that performs the conversion:
cast_to_int(0.7) -> 0
cast_to_int(1.0) -> 1
cast_to_int(1.5) -> 1
cast_to_int(2.1) -> 2
In the second case, reinterpret_cast just directly transforms the underlying representation of the bits. It's not actually doing anything, and the function just copyies the input register to the output register.
Under the hood, floats have a very different bit representation than ints, and that's why you're getting weird numbers.
Nobody can explain (*), because it does not work.
From cppreference:
Unlike static_cast, but like const_cast, the reinterpret_cast
expression does not compile to any CPU instructions (except when
converting between integers and pointers or on obscure architectures
where pointer representation depends on its type). It is purely a
compile-time directive which instructs the compiler to treat
expression as if it had the type new_type.
Only the following conversions can be done with reinterpret_cast,
except when such conversions would cast away constness or volatility.
And then follows a list of rules covering what reinterpret casts are allowed. Casting type A to a completely unrelated type B is not among them and your code exhibits undefined behaviour.
(*) Strictly speaking not true. You are treating a float as an int and if you look at their representation on your hardware and if you inspect the output of your compiler you can work out why you get the value you get, though undefined behaviour is undefined and its not worth entering details unless you are willing to write nonportable code.
As the comments below say, the usage of reinterpret_cast ist not safe for unrelated types. So don't use it this way.
First, assigning int to uint should be avoided.
For x86/x64 systemns, float is usually represented as 4 bytes, like uint32_t, and they have mostely the same alignment.
Many compilers allow the following:
uint32_t _f = *reinterpret_cast(&f);
But this leads to undefined behavior due to some reasons (thanks for the comments):
- optimizations
- alignment
- ...
Use memcpy instead.
If alignment is the same and the values are stored in memory, the following effect desribes what happens when used reinterpret_cast:
The memory location of the 4 bytes float is &f. With a reinterpret cast to uint32_t, this memory is reinterpretet as an uint32. The dereferenced value _f contains the same bytes as the float f, but interpreted as uint32.
You could cast it back and get the original value 12.5:
float f = 12.5;
uint32_t _f = *reinterpret_cast<uint32_t*>(&f);
float _fnew = *reinterpret_cast<float*>(&_f);
std::cout << _fnew << std::endl; // result: 12.5
reinterpret_cast only reinterpretes memory locations (addresses).
If the alignment is not the same or the values are stored in registers, are optimized an so on, the cast can lead to undefined values.
What I know about pointer is, it is used to point to specific location (memory address), so why do we even need the same data type of pointer as that of the variable we are trying to point.
Suppose I create a variable of integer, then I have to create a pointer to integer to point it. So why can't I create a void pointer or float pointer to point the value stored in that integer variable!
Am I missing some concepts of pointers?
So why can't I create a void pointer [...] to point the value stored in that integer variable
You can do that, no problem:
int x = 10;
double y = 0.4;
void* v = &x;
v = &y;
But now imagine a function like this:
void print(void* value)
How would this function know what to do with the memory at the pointer location? Is it an integer? Or a floating point number? A float or a double? Maybe it's a huge struct or an array of values? You must know this to dereference the pointer (i.e. read the memory) correctly, so it only makes sense to have different pointer types for pointers to different types:
void print(int* value)
This function knows that the pointer points to an int, so it can happily dereference it to get an int value.
The pointer type is also important when dealing with arrays, as arrays and pointers are interchangeable. When you increment a pointer (which is what indexing does), you need to know how big the type is (int, long, structure, class) in order to access the next item.
arr[5] == *(arr+5) but 5 what? This is determined by the type.
A small addition on Max Langhof's answer:
It is important to realise that in the end, variables are stored simply as a sequence of bits (binary digits), e.g. 01010101 00011101 11100010 11110000. How does your program know what this 'means'? It could be an integer (which is often 4 bytes on modern architectures), it could be a floating-point value. For the memory involved this makes no difference, but for your code the implications can be huge. Therefore, if you refer to this memory location (using a pointer), you will need to specify how the bytes there should be converted to decimal (or other) values.
Pointer arithmetic is the main reason - if p points to an object of type T, then p+1 points to the next object of that type. If p points to a 4-byte int, then p+1 points to the following 4-byte int. If p points to a 128-byte struct, then p+1 points to the following 128-byte struct. If p points to a 2 Kbyte array of double, then p+1 points to the next 2 Kbyte array of double, etc.
But it's also for the same reason we have different types in the first place - at the abstract machine level, we want to distinguish different types of data and operations that are allowed on that data. A pointer to an int is different from a pointer to a double because an int is different from a double.
You are right. Although int and float are different types, there should be no difference between their pointers int* and float*. In general, this is the case. However, the binary representation is different between int and float. Therefore accessing an int with a float* pointer leads to garbage being read from the RAM.
Furthermore, what you have on your machine is not the general case, but hardware and implementation dependent.
For example: float and int variables are usually 32bit long. However, there are systems where the int has only 16bit. What happens now if you try to read a float from a int* pointer? (Or, even if both are 32bit, what happens if you try to read a float from a char*?)
Memory accesses do not work without knowing what kind of data object you are dealing with.
Imagine some simple assignment:
int a, b=10;
float f;
a = b; // same type => Just copy the integer
f = b; // wrong type => Convert to float.
This works fine because the compiler knows that both variables are of a certain type and size and representation. If the types do not match, a proper conversion is applied.
Now the same with typed pointers:
int a = 10;
float f;
int *pa;
float *pf;
f = a; // Type conversion to float applied
*pa = a; // Just copy
*pf = a; // Type conversion
If you take away the knowledge about the memory location where the pointer points to, how would the compiler know if a conversion is required?
Or if some integer propagation is needed or is an integer is truncated into a shorter type?
More problems are waiting around the corner if you want to use a pointer to address elements of an array. Pointer arithmetics won't fly without types.
Types are essential. For variables as well as for pointers.
void* is a useful feature of C and derivative languages. For example, it's possible to use void* to store objective-C object pointers in a C++ class.
I was working on a type conversion framework recently and due to time constraints was a little lazy - so I used void*... That's how this question came up:
Why can I typecast int to void*, but not float to void* ?
BOOL is not a C++ type. It's probably typedef or defined somewhere, and in these cases, it would be the same as int. Windows, for example, has this in Windef.h:
typedef int BOOL;
so your question reduces to, why can you typecast int to void*, but not float to void*?
int to void* is ok but generally not recommended (and some compilers will warn about it) because they are inherently the same in representation. A pointer is basically an integer that points to an address in memory.
float to void* is not ok because the interpretation of the float value and the actual bits representing it are different. For example, if you do:
float x = 1.0;
what it does is it sets the 32 bit memory to 00 00 80 3f (the actual representation of the float value 1.0 in IEEE single precision). When you cast a float to a void*, the interpretation is ambiguous. Do you mean the pointer that points to location 1 in memory? or do you mean the pointer that points to location 3f800000 (assuming little endian) in memory?
Of course, if you are sure which of the two cases you want, there is always a way to get around the problem. For example:
void* u = (void*)((int)x); // first case
void* u = (void*)(((unsigned short*)(&x))[0] | (((unsigned int)((unsigned short*)(&x))[1]) << 16)); // second case
Pointers are usually represented internally by the machine as integers. C allows you to cast back and forth between pointer type and integer type. (A pointer value may be converted to an integer large enough to hold it, and back.)
Using void* to hold integer values in unconventional. It's not guaranteed by the language to work, but if you want to be sloppy and constrain yourself to Intel and other commonplace platforms, it will basically scrape by.
Effectively what you're doing is using void* as a generic container of however many bytes are used by the machine for pointers. This differs between 32-bit and 64-bit machines. So converting long long to void* would lose bits on a 32-bit platform.
As for floating-point numbers, the intention of (void*) 10.5f is ambiguous. Do you want to round 10.5 to an integer, then convert that to a nonsense pointer? No, you want the bit-pattern used by the FPU to be placed into a nonsense pointer. This can be accomplished by assigning float f = 10.5f; void *vp = * (uint32_t*) &f;, but be warned that this is just nonsense: pointers aren't generic storage for bits.
The best generic storage for bits is char arrays, by the way. The language standards guarantee that memory can be manipulated through char*. But you have to mind data alignment requirements.
Standard says that 752 An integer may be converted to any pointer type. Doesn't say anything about pointer-float conversion.
Considering any of you want you transfer float value as void *, there is a workaround using type punning.
Here is an example;
struct mfloat {
union {
float fvalue;
int ivalue;
};
};
void print_float(void *data)
{
struct mfloat mf;
mf.ivalue = (int)data;
printf("%.2f\n", mf.fvalue);
}
struct mfloat mf;
mf.fvalue = 1.99f;
print_float((void *)(mf.ivalue));
we have used union to cast our float value(fvalue) as an integer(ivalue) to void*, and vice versa
The question is based on a false premise, namely that void * is somehow a "generic" or "catch-all" type in C or C++. It is not. It is a generic object pointer type, meaning that it can safely store pointers to any type of data, but it cannot itself contain any type of data.
You could use a void * pointer to generically manipulate data of any type by allocating sufficient memory to hold an object of any given type, then using a void * pointer to point to it. In some cases you could also use a union, which is of course designed to be able to contain objects of multiple types.
Now, because pointers can be thought of as integers (and indeed, on conventionally-addressed architectures, typically are integers) it is possible and in some circles fashionable to stuff an integer into a pointer. Some library API's have even documented and supported this usage — one notable example was X Windows.
Conversions between pointers and integers are implementation-defined, and these days typically draw warnings, and so typically require an explicit cast, not so much to force the conversion as simply to silence the warning. For example, both the code fragments below print 77, but the first one probably draws compiler warnings.
/* fragment 1: */
int i = 77;
void *p = i;
int j = p;
printf("%d\n", j);
/* fragment 2: */
int i = 77;
void *p = (void *)(uintptr_t)i;
int j = (int)p;
printf("%d\n", j);
In both cases, we are not really using the void * pointer p as a pointer at all: we are merely using it as a vessel for some bits. This relies on the fact that on a conventionally-addressed architecture, the implementation-defined behavior of a pointer/integer conversion is the obvious one, which to an assembly-language programmer or an old-school C programmer doesn't seem like a "conversion" at all. And if you can stuff an int into a pointer, it's not surprising if you can stuff in other integral types, like bool, as well.
But what about trying to stuff a floating-point value into a pointer? That's considerably more problematic. Stuffing an integer value into a pointer, though implementation-defined, makes perfect sense if you're doing bare-metal programming: you're taking the numeric value of the integer, and using it as a memory address. But what would it mean to try to stuff a floating-point value into a pointer?
It's so meaningless that the C Standard doesn't even label it "undefined".
It's so meaningless that a typical compiler won't even attempt it.
And if you think about it, it's not even obvious what it should do.
Would you want to use the numeric value, or the bit pattern, as the thing to try to stuff into the pointer? Stuffing in the numeric value is closer to how floating-point-to-integer conversions work, but you'd lose your fractional part. Using the bit pattern is what you'd probably want, but accessing the bit pattern of a floating-point value is never something that C makes easy, as generations of programmers who have attempted things like
uint32_t hexval = (uint32_t)3.0;
have discovered.
Nevertheless, if you were bound and determined to store a floating-point value in a void * pointer, you could probably accomplish it, using sufficiently brute-force casts, although the results are probably both undefined and machine-dependent. (That is, I think there's a strict aliasing violation here, and if pointers are bigger than floats, as of course they are on a 64-bit architecture, I think this will probably only work if the architecture is little-endian.)
float f = 77.75;
void *p = (void *)(uintptr_t)*(uint32_t *)&f;
float f2 = *(float *)&p;
printf("%f\n", f2);
dmr help me, this actually does print 77.75 on my machine.
How do type casting happen without loss of data inside the compiler?
For example:
int i = 10;
UINT k = (UINT) k;
float fl = 10.123;
UINT ufl = (UINT) fl; // data loss here?
char *p = "Stackoverflow Rocks";
unsigned char *up = (unsigned char *) p;
How does the compiler handle this type of typecasting? A low-level example showing the bits would be highly appreciated.
Well, first note that a cast is an explicit request to convert a value of one type to a value of another type. A cast will also always produce a new object, which is a temporary returned by the cast operator. Casting to a reference type, however, will not create a new object. The object referenced by the value is reinterpreted as a reference of a different type.
Now to your question. Note that there are two major types of conversions:
Promotions: This type can be thought of casting from a possibly more narrow type to a wider type. Casting from char to int, short to int, float to double are all promotions.
Conversions: These allow casting from long to int, int to unsigned int and so forth. They can in principle cause loss of information. There are rules for what happens if you assign a -1 to an unsigned typed object for example. In some cases, a wrong conversion can result in undefined behavior. If you assign a double larger than what a float can store to a float, the behavior is not defined.
Let's look at your casts:
int i = 10;
unsigned int k = (unsigned int) i; // :1
float fl = 10.123;
unsigned int ufl = (unsigned int) fl; // :2
char *p = "Stackoverflow Rocks";
unsigned char *up = (unsigned char *) p; // :3
This cast causes a conversion to happen. No loss of data happens, since 10 is guaranteed to be stored by an unsigned int. If the integer were negative, the value would basically wrap around the maximal value of an unsigned int (see 4.7/2).
The value 10.123 is truncated to 10. Here, it does cause lost of information, obviously. As 10 fits into an unsigned int, the behavior is defined.
This actually requires more attention. First, there is a deprecated conversion from a string literal to char*. But let's ignore that here. (see here). More importantly, what does happen if you cast to an unsigned type? Actually, the result of that is unspecified per 5.2.10/7 (note the semantics of that cast is the same as using reinterpret_cast in this case, since that is the only C++ cast being able to do that):
A pointer to an object can be explicitly converted to a pointer to
an object of different type. Except that converting an rvalue of type “pointer to T1” to the type "pointer to T2" (where T1 and T2 are object types and where the alignment requirements of T2 are no stricter than those of T1) and back to its original type yields the original pointer value, the result of such a pointer conversion is unspecified.
So you are only safe to use the pointer after you cast back to char * again.
The two C-style casts in your example are different kinds of cast. In C++, you'd normally write them
unsigned int uf1 = static_cast<unsigned int>(fl);
and
unsigned char* up = reinterpret_cast<unsigned char*>(p);
The first performs an arithmetic cast, which truncates the floating point number, so there is data loss.
The second makes no changes to data - it just instructs the compiler to treat the pointer as a different type. Care needs to be taken with this kind of cast: it can be very dangerous.
"Type" in C and C++ is a property assigned to variables when they're handled in the compiler. The property doesn't exist at runtime anymore, except for virtual functions/RTTI in C++.
The compiler uses the type of variables to determine a lot of things. For instance, in the assignment of a float to an int, it will know that it needs to convert. Both types are probably 32 bits, but with different meanings. It's likely that the CPU has an instruction, but otherwise the compiler would know to call a conversion function. I.e.
& __stack[4] = float_to_int_bits(& __stack[0])
The conversion from char* to unsigned char* is even simpeler. That is just a different label. At bit level, p and up are identical. The compiler just needs to remember that *p requires sign-extension while *up does not.
Casts mean different things depending on what they are. They can just be renamings of a data type, with no change in the bits represented (most casts between integral types and pointers are like this), or conversions that don't even preserve length (such as between double and int on most compilers). In many cases, the meaning of a cast is simply unspecified, meaning the compiler has to do something reasonable but doesn't have to document exactly what.
A cast doesn't even need to result in a usable value. Something like
char * cp;
float * fp;
cp = malloc(100);
fp = (float *)(cp + 1);
will almost certainly result in a misaligned pointer to float, which will crash the program on some systems if the program attempts to use it.