Does casting actually DO anything?

此生再无相见时 提交于 2021-02-20 17:18:09


Consider the following snippet:

char x[100];
double *p = &x;

As expected, this yields this warning:

f.c:3:15: warning: initialization of ‘double *’ from incompatible pointer type ‘char (*)[100]’ 
    3 |   double *p = &x;
      |               ^

This is very easy to solve by just changing to

double *p = (double*)&x;

My question here is, does the casting actually DO anything? Would the code be invalid without the cast? Or is it just a way to make the compiler quiet? When is casting necessary?

I know that you can have some effect with snippets like this:

int x = 666;
int y = (char)x;

But isn't this the same as this?

int x = 666;
char c = x;
int y = c;

If it is the same, then the casting does something, but it's not necessary. Right?

Please help me understand this.


Casting can do several different things. As other answers have mentioned, it almost always changes the type of the value being cast (or, perhaps, an attribute of the type, such as const). It may also change the numeric value in some way. But there are many possible interpretations:

  • Sometimes it merely silences a warning, performing no "real" conversion at all (as in many pointer casts).
  • Sometimes it silences a warning, leaving only a type change but no value change (as in other pointer casts).
  • Sometimes the type change, although it involves no obvious value change, implies very different semantics for use of the value later (again, as in many pointer casts).
  • Sometimes it requests a conversion which is meaningless or impossible.
  • Sometimes it performs a conversion that the compiler would have performed by itself (with or without a warning).
  • But sometimes it forces a conversion that the compiler wouldn't have performed.

Also, sometimes those warnings that the compiler tried to make, that a cast silences, are innocuous and/or a nuisance, but sometimes they're quite real, and the code is likely to fail (that is, as the silenced warning was trying to tell you).

For some more specific examples:

A pointer cast that changes the type, but not the value:

char *p1 = ... ;
const char *p2 = (const char *)p;

And another:

unsigned char *p3 = (unsigned char *)p;

A pointer cast that changes the type in a more significant way, but that's guaranteed to be okay (on some architectures this might also change the value):

int i;
int *ip = &i;
char *p = (char *)ip;

A similarly significant pointer cast, but one that's quite likely to be not okay:

char c;
char *cp = &c;
int *ip = (int *)cp;
*ip = 5;                  /* likely to fail */

A pointer cast that's so meaningless that the compiler refuses to perform it, even with an explicit cast:

float f = 3.14;
char *p = (char)f;        /* guaranteed to fail */

A pointer cast that makes a conversion, but one that the compiler would have made anyway:

int *p = (int *)malloc(sizeof(int));

(This one is considered a bad idea, because in the case where you forget to include <stdlib.h> to declare malloc(), the cast can silence a warning that might alert you to the problem.)

Three casts from an integer to a pointer, that are actually well-defined, due to a very specific special case in the C language:

void *p1 = (void *)0;
char *p2 = (void *)0;
int *p3 = (int *)0;

Two casts from integer to pointer that are not necessarily valid, although the compiler will generally do something obvious, and the cast will silence the otherwise warning:

int i = 123;
char *p1 = (char *)i;
char *p2 = (char *)124;
*p1 = 5;                  /* very likely to fail, except when */
*p2 = 7;                  /* doing embedded or OS programming */

A very questionable cast from a pointer back to an int:

char *p = ... ;
int i = (int)p;

A less-questionable cast from a pointer back to an integer that ought to be big enough:

char *p = ... ;
uintptr_t i = (uintptr_t)p;

A cast that changes the type, but "throws away" rather than "converting" a value, and that silences a warning:


A cast that makes a numeric conversion, but one that the compiler would have made anyway:

float f = (float)0;

A cast that changes the type and the interpreted value, although it typically won't change the bit pattern:

short int si = -32760;
unsigned short us = (unsigned short)si;

A cast that makes a numeric conversion, but one that the compiler probably would have warned about:

int i = (int)1.5;

A cast that makes a conversion that the compiler would not have made:

double third = (double)1 / 3;

The bottom line is that casts definitely do things: some of them useful, some of them unnecessary but innocuous, some of them dangerous.

These days, the consensus among many C programmers is that most casts are or should be unnecessary, meaning that it's a decent rule to avoid explicit casts unless you're sure you know what you're doing, and it's reasonable to be suspicious of explicit casts you find in someone else's code, since they're likely to be a sign of trouble.

As one final example, this was the case that, back in the day, really made the light bulb go on for me with respect to pointer casts:

char *loc;
int val;
int size;

/* ... */

switch(size) {
    case 1: *loc += val; break;
    case 2: *(int16_t *)loc += val; break;
    case 4: *(int32_t *)loc += val; break;

Those three instances of loc += val do three pretty different things: one updates a byte, one updates a 16-bit int, and one updates a 32-bit int. (The code in question was a dynamic linker, performing symbol relocation.)


The cast does at least 1 thing - it satisfies the following constraint on assignment: Simple assignment


1    One of the following shall hold:112)
— the left operand has atomic, qualified, or unqualified pointer type, and (considering the type the left operand would have after lvalue conversion) both operands are pointers to qualified or unqualified versions of compatible types, and the type pointed to by the left has all the qualifiers of the type pointed to by the right;
112) The asymmetric appearance of these constraints with respect to type qualifiers is due to the conversion (specified in that changes lvalues to ‘‘the value of the expression’’ and thus removes any type qualifiers that were applied to the type category of the expression (for example, it removes const but not volatile from the type int volatile * const).

That's a compile-time constraint - it affects whether or not the source code is translated to an executable, but it doesn't necessarily affect the translated machine code.

It may result in an actual conversion being performed at runtime, but that depends on the types involved in the expression and the host system.


Casting changes the type, which can be very important when signed or unsigned type matters,

For example, character handling functions such as isupper() are defined as taking an unsigned char value or EOF:

The header <ctype.h> declares several functions useful for classifying and mapping characters. In all cases the argument is an int, the value of which shall be representable as an unsigned char or shall equal the value of the macro EOF. If the argument has any other value, the behavior is undefined.

Thus code such as

int isNumber( const char *input )
    while ( *input )
        if ( !isdigit( *input ) )
             return( 0 );
    // all digits
    return( 1 );

should properly cast the const char value of *input to unsigned char:

int isNumber( const char *input )
    while ( *input )
        if ( !isdigit( ( unsigned char ) *input ) )
             return( 0 );
    // all digits
    return( 1 );

Without the cast to unsigned char, when *input is promoted to int, an char value (assuming char is signed and smaller than int) that is negative will be sign-extended to a negative value that can not be represented as an unsigned char value and therefore invoke undefined behavior.

So yes, the cast in this case does something. It changes the type and therefore - on almost all current systems - avoids undefined behavior for input char values that are negative.

There are also cases where float values can be cast to double (or the reverse) to force code to behave in a desired manner.*

* - I've seen such cases recently - if someone can find an example, feel free to add your own answer...


The cast may or may not change the actual binary value. But that is not its main purpose, just a side effect.

It tells the compiler to interpret a value as a value of a different type. Any changing of binary value is a side effect of that.

It is for you (the programmer) to let the compiler know: I know what I'm doing. So you can shoot yourself in the foot without the compiler questioning you.

Don't get me wrong, cast are absolutely necessary in real world code, but they must be used with care and knowledge. Never cast just to get rid of a warning, make sure you understand the consequences.


It is theoretically possible that a system would use a different representation for a void * and a char * than for some other pointer type. The possibility exists that there could be a system that normally uses a narrow width register to hold pointer values. But, the narrow width may be insufficient if the code needed to address every single byte, and so a void * or char * would use a wider representation.

One case where casting a pointer value is useful is when the function takes a variable number of pointer arguments, and is terminated by NULL, such as with the execl() family of functions.

execl("/bin/sh", "sh", "-c", "echo Hello world!", (char *)NULL);

Without the cast, the NULL may expand to 0, which would be treated as an int argument. When the execl() function retrieves the last parameter, it may extract the expected pointer value incorrectly, since an int value was passed.


My question here is, does the casting actually DO anything?

Yes. It tells the compiler, and also other programmers including the future you, that you think you know what you're doing and you really do intend to treat a char as an int or whatever. It may or may not change the compiled code.

Would the code be invalid without the cast?

That depends on the cast in question. One example that jumps to mind involves division:

int a = 3;
int b = 5;
float c = a / b;

Questions about this sort of thing come up all the time on SO: people wonder why c gets a value of 0. The answer, of course, is that both a and b are int, and the result of integer division is also an integer that's only converted to a float upon assignment to c. To get the expected value of 0.6 in c, cast a or b to float:

float c = a / (float)b;

You might not consider the code without the cast to be invalid, but the next computation might involve division by c, at which point a division by zero error could occur without the cast above.

Or is it just a way to make the compiler quiet?

Even if the cast is a no-op in terms of changing the compiled code, preventing the compiler from complaining about a type mismatch is doing something.

When is casting necessary?

It's necessary when it changes the object code that the compiler generates. It might also be necessary if your organization's coding standards require it.


An example where cast makes a difference.

int main(void)
    unsigned long long x = 1 << 33,y = (unsigned long long)1 << 33;

    printf("%llx, %llx\n", x, y);


A cast is simply a type conversion: The implementation will represent the argument value by means of the target type. The expression of the new type (let's assume the target type is different) may have

  • a different size
  • a different value
  • and/or a different bit pattern representing the value.

These three changes are orthogonal to each other. Any subset, including none and all of them, can occur (all examples assume two's bit complement):

  • None of them: (unsigned int)1;
  • Size only: (char)1
  • Value only: (unsigned int)-1
  • Bit pattern only: (float)1 (my machine has sizeof(int) == sizeof(float))
  • Size and value, but not bit pattern (the bits present in the original value): (unsigned int)(char)-4
  • Size and bit pattern, but not value: (float)1l
  • value and bit pattern, but not size: (float)1234567890) (32 bit ints and floats)
  • All of them: (float)1234567890l (long is 64 bits).

The new type may, of course, influence expressions in which it is used, and will often have different text representations (e.g. via printf), but that's not really surprising.

Pointer conversions may deserve a little more discussion: The new pointer typically has the same value, size and bit representation (although, as Eric Postpischli correctly pointed out, it theoretically may not). The main intent and effect of a pointer cast is not to change the pointer; it is to change the semantics of the memory accessed through it.

In some cases a cast is the only means to perform a conversion (non-compatible pointer types, pointer vs. integer).

In other cases like narrowing arithmetic conversions (which may lose information or produce an overflow) a cast indicates intent and thus silences warnings. In all cases where a conversion could also be implicitly performed the cast does not alter the program's behavior — it is optional decoration.