Is const-casting via a union undefined behaviour? - c

Unlike C++, C has no notion of a const_cast. That is, there is no valid way to convert a const-qualified pointer to an unqualified pointer:
void const * p;
void * q = p; // not good
First off: Is this cast actually undefined behaviour?
In any event, GCC warns about this. To make "clean" code that requires a const-cast (i.e. where I can guarantee that I won't mutate the contents, but all I have is a mutable pointer), I have seen the following "conversion" trick:
typedef union constcaster_
{
void * mp;
void const * cp;
} constcaster;
Usage: u.cp = p; q = u.mp;.
What are the C language rules on casting away constness through such a union? My knowledge of C is only very patchy, but I've heard that C is far more lenient about union access than C++, so while I have a bad feeling about this construction, I would like an argument from the standard (C99 I suppose, though if this has changed in C11 it'll be good to know).

It's implementation defined, see C99 6.5.2.3/5:
if the value of a member of a union object is used when the most
recent store to the object was to a different member, the behavior is
implementation-defined.
Update: #AaronMcDaid commented that this might be well-defined after all.
The standard specified the following 6.2.5/27:
Similarly, pointers to qualified or unqualified versions of compatible
types shall have the same representation and alignment
requirements.27)
27) The same representation and alignment requirements are meant to
imply interchangeability as arguments to functions, return values from
functions, and members of unions.
And (6.7.2.1/14):
A pointer to a union object, suitably converted, points to each of its
members (or if a member is a bitfield, then to the unit in which it
resides), and vice versa.
One might conclude that, in this particular case, there is only room for exactly one way to access the elements in the union.

My understanding it that the UB can arise only if you try to modify a const-declared object.
So the following code is not UB:
int x = 0;
const int *cp = &x;
int *p = (int*)cp;
*p = 1; /* OK: x is not a const object */
But this is UB:
const int cx = 0;
const int *cp = &cx;
int *p = (int*)cp;
*p = 1; /* UB: cx is const */
The use of a union instead of a cast should not make any difference here.
From the C99 specs (6.7.3 Type qualifiers):
If an attempt is made to modify an object defined with a const-qualified type through use
of an lvalue with non-const-qualified type, the behavior is undefined.

The initialization certainly won't cause UB. The conversion between qualified pointer types is explicitly allowed in §6.3.2.3/2 (n1570 (C11)). It's the use of content in that pointer afterwards that cause UB (see #rodrigo's answer).
However, you need an explicit cast to convert a void* to a const void*, because the constraint of simple assignment still require all qualifier on the LHS appear on the RHS.
§6.7.9/11: ... The initial value of the object is that of the expression (after conversion); the same type constraints and conversions as for simple assignment apply, taking the type of the scalar to be the unqualified version of its declared type.
§6.5.16.1/1: (Simple Assignment / Contraints)
... 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;
... one operand is a pointer
to an object type, and the other is a pointer to a qualified or unqualified version of
void, and the type pointed to by the left has all the qualifiers of the type pointed to
by the right;
I don't know why gcc just gives a warning though.
And for the union trick, yes it's not UB, but still the result is probably unspecified.
§6.5.2.3/3 fn 95: If the member used to read the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called "type punning"). This might be a trap representation.
§6.2.6.1/7: When a value is stored in a member of an object of union type, the bytes of the object representation that do not correspond to that member but do correspond to other members take unspecified values. (* Note: see also §6.5.2.3/6 for an exception, but it doesn't apply here)
The corresponding sections in n1124 (C99) are
C11 §6.3.2.3/2 = C99 §6.3.2.3/2
C11 §6.7.9/11 = C99 §6.7.8/11
C11 §6.5.16.1/1 = C99 §6.5.16.1/1
C11 §6.5.2.3/3 fn 95 = missing ("type punning" doesn't appear in C99)
C11 §6.2.6.1/7 = C99 §6.2.6.1/7

Don't cast it at all. It's a pointer to const which means that attempting to modify the data is not allowed and in many implementations will cause the program to crash if the pointer points to unmodifiable memory. Even if you know the memmory can be modified, there may be other pointers to it that do not expect it to change e.g. if it is part of the storage of a logically immutable string.
The warning is there for good reason.
If you need to modify the content of a const pointer, the portable safe way to do it is first to copy the memory it points to and then modify that.

Related

Comparing struct pointers, casting away members, and UB

Consider the following code:
int main()
{
typedef struct { int first; float second; } type;
type whole = { 1, 2.0 };
void * vp = &whole;
struct { int first; } * shorn = vp;
printf("values: %d, %d\n", ((type *)vp)->first, shorn->first);
if (vp == shorn)
printf("ptrs compare the same\n");
return 0;
}
Two questions:
Is the pointer equality comparison UB?
Regarding the "shearing" away of the second member on the line that initializes shorn: is it valid C to cast away struct members like this and then dereference the manipulated pointer to access the remaining member?
Comparing two pointers with == when one is a void * is well defined.
Section 6.5.9 of the C standard regarding the equality operator == says the following:
2 One of the following shall hold:
both operands have arithmetic type;
both operands are pointers to qualified or unqualified versions of compatible types;
one operand is a pointer to an object type and the other is a pointer to a qualified or unqualified version of void; or
one operand is a pointer and the other is a null pointer constant
...
5 Otherwise, at least one operand is a pointer. If one operand is a pointer and the other is a null pointer constant, the null pointer
constant is converted to the type of the pointer. If one operand
is a pointer to an object type and the other is a pointer
to a qualified or unqualified version of void, the former is
converted to the type of the latter.
The usage of shorn->first works because a pointer to a struct can be converted to a pointer to its first member. For both type and the unnamed struct type their first member is an int so it works out.
Section 6.2.5 Types paragraph 28 of the C standard says:
[...] All pointers to structure types shall have the same representation and alignment requirements as each other. [...]
Section 6.3.2.3 Pointers paragraph 1 says:
A pointer to void may be converted to or from a pointer to any object type. A pointer to any object type may be converted to a pointer to void and back again; the result shall compare equal to the original pointer.
And paragraph 7 says:
A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned68) for the referenced type, the behavior is undefined. Otherwise, when converted back again, the result shall compare equal to the original pointer. [...]
And footnote 68 says:
In general, the concept "correctly aligned" is transitive: if a pointer to type A is correctly aligned for a pointer to type B, which in turn is correctly aligned for a pointer to type C, then a pointer to type A is correctly aligned for a pointer to type C.
Because all pointers to structure types have the same representation, the conversions between pointers to void and pointers to structure types must be the same for all pointers to structure types. So it seems that a pointer to structure type A could be converted by a cast operator directly to a pointer to structure type B without an intermediate conversion to a pointer to void as long as the pointer is "correctly aligned" for structure type B. (This may be a weak argument.)
The question remains when, in the case of two structure types A and B where the initial sequence of structure type A consists of all the members of structure type B, a pointer to structure type A is guaranteed to be correctly aligned for structure type B (the reverse is obviously not guaranteed). As far as I can tell, the C standard makes no such guarantee. So strictly speaking, a pointer to the larger structure type A might not be correctly aligned for the smaller structure type B, and if it is not, the behavior is undefined. For a "sane" compiler, the larger structure type A would not have weaker alignment than the smaller structure type B, but for an "insane" compiler, that might not be the case.
Regarding the second question about accessing members of the truncated (shorter) structure using the pointer derived from the full (longer) structure, then as long as the pointer is correctly aligned for the shorter structure (see above for why that might not be true for an "insane" compiler), and as long as strict aliasing rules are avoided (for example, by going through an intermediate pointer to void in an intermediate external function call across compilation unit boundaries), then accessing the members through the pointer to the shorter structure type should be perfectly fine. There is a special guarantee for that when objects of both structure types appear as members of the same union type. Section 6.3.2.3 Structure and union members paragraph 6 says:
One special guarantee is made in order to simplify the use of unions: if a union contains several structures that share a common initial sequence (see below), and if the union
object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the completed type of the union
is visible. Two structures share a common initial sequence if corresponding members have compatible types (and, for bit-fields, the same widths) for a sequence of one or more initial members.
However, since the offsets of members within a structure type does not depend on whether an object of the structure type appears in a union type or not, the above implies that any structures with a common initial sequence of members will have those common members at the same offsets within their respective structure types.
In the language the C89 Standard was written to describe, it was well established that if two structures share a common initial sequence, a pointer to either may be cast to the other and used to inspect members of that common initial sequence. Code which relied upon this was commonplace and not considered even remotely controversial.
In the interest of optimization, the authors of the C99 Standard deliberately allowed compilers to assume that structures of different types won't alias in cases where such assumption would be useful for their customers. Because there are many good means by which implementations could recognize cases where such assumptions would be needlessly break what had been perfectly good code, and because they the authors of the Standard expected that compiler writers would make a bona fide effort to behave in ways useful to the programmers using their products, the Standard doesn't mandate any particular means of making such distinctions. Instead, it regards the ability to support constructs that had been universally supported as a "quality of implementation" issue, which would be reasonable if compiler writers made a bona fide effort to treat it as such.
Unfortunately, some compiler writers who aren't interested in selling their products to paying customers have taken the Standard's failure to mandate useful behavior as an invitation to behave in needlessly useless fashion. Code which relies upon Common Initial Sequence guarantees can thus not be meaningfully processed by clang or gcc without using either non-standard syntax or disabling type-based aliasing entirely.

Implicit Pointer-To-Const-T conversion

I was wondering if the implicit conversion to a pointer to a const data type is somewhere defined in the C11 standard:
T x;
const T *p = &x;
A pointer to an object of type T is implicitly converted into a pointer to an object of type const T. Is this implicit conversion somewhere defined in the C11 standard? (I know that it makes sense to allow this and how useful it is. I'm just curious to know where it is defined in the standard)
Furthermore, is an implicit conversion from type T** to const T** forbidden according to C11?
T *p;
const T **pp = &p;
This is a well known problematic part and therefore GCC and LLVM/clang raise a warning. Still I'm wondering if this is allowed according to the C11 standard or not. I only found in §6.5.16.1P6 a comment that this should be a constraint violation. However, I do not see which constraint should be violated. Again I know that this should be prohibited and that this implicit conversion can lead to subtle problems. I'm just curious to know if this is (un)defined behaviour according to C11.
Again, my two questions are not about if this is good or not (which is answered multiple times here) but how/where the C11 standard defines this.
Just for the sake of completeness here is a link to why the second example is problematic: http://c-faq.com/ansi/constmismatch.html
A pointer to an object of type T is implicitely converted into a
pointer to an object of type const-T. Is this implicit conversion
somewhere defined in the C11 standard?
Yes. This implicit conversion is mandated by the standard.
Paragraph 3 of section 6.5.4 Cast Operators says that
Conversions that involve pointers, other than where permitted by the
constraints of 6.5.16.1, shall be specified by means of an explicit
cast.
and the referenced 6.5.16.1 under point 3 says:
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;
Therefore the implicit conversion for const T *p = &x; holds because you're only adding qualifiers, not removing them.
const T **pp = &p; doesn't fall under this, so you need an explicit cast (C++ would allow const T*const*pp = &p; (the second const is needed) but C still wouldn't.)
The pointer conversion through an explicit cast isn't a problem as far as UB is concerned as long as the alignments match (which for pointers to differently qualified types they will) because
6.3.2.3p7 guarantees that:
A pointer to an object type may be converted to a pointer to a
different object type. If the resulting pointer is not correctly
aligned68) for the referenced type, the behavior is undefined.
Otherwise, when converted back again, the result shall compare equal
to the original pointer. When a pointer to an object is converted to a
pointer to a character type, the result points to the lowest addressed
byte of the object. Successive increments of the result, up to the
size of the object, yield pointers to the remaining bytes of the
object.
but you need to be mindful of accesses/dereferences which will be governed by the strict aliasing rule:
An object shall have its stored value accessed only by an lvalue
expression that has one of the following types:88)
a type compatible with the effective type of the object, a qualified
version of a type compatible with the effective type of the object, a
type that is the signed or unsigned type corresponding to the
effective type of the object, a type that is the signed or unsigned
type corresponding to a qualified version of the effective type of the
object, an aggregate or union type that includes one of the
aforementioned types among its members (including, recursively, a
member of a subaggregate or contained union), or a character type.

Reading through an char array passed as void* with pointer incrementation and later read as chars and other datatypes?

So to clear out misunderstandings from the title (not sure how to ask the question in the title) I want to read from a file(char array), pass it as an void* so i can read undependable of datatype by incrementing the pointer. So here's an simple example of what I want to do in C code:
char input[] = "D\0\0Ckjh\0";
char* pointer = &input[0]; //lets say 0x00000010
char type1 = *pointer; //should be 'D'
pointer += sizeof(char); //0x00000020
uint16_t value1 = *(uint16_t*)pointer; //should be 0
pointer += sizeof(uint16_t); //0x00000040
char type2 = *pointer; //should be 'C'
pointer += sizeof(char); //0x00000050
uint32_t value2 = *(uint32_t*)pointer; //should be 1802135552
This is just for educational purpose, so I would just like to know if it is possible or if there is a way to achieve the same goal or something alike. Also the speed of this would be nice to know. Would it be faster to just keep the array and just make bitshifting on the chars as you read them or is this actually faster?
Edit: edit on the c code and changed void* to char*;
This is wrong in two ways:
void is an incomplete type that cannot be completed. An incomplete type is a type without a known size. In order to do pointer arithmetics, the size must be known. The same is true for dereferencing a pointer. Some compilers attribute the size of a char to void, but that's an extension you should never rely on. Incrementing a pointer to void is wrong and can't work.
What you have is an array of char. Accessing this array through a pointer of a different type violates strict aliasing, you're not allowed to do that.
That's actually not what your current code does -- looking at this line:
uint32_t value2 = (int)*pointer; //should be 1802135552
You're just converting the single byte (assuming your pointer points to char, see my first point) to an uint32_t. What you probably meant is
uint32_t value2 = *(uint32_t *)pointer; //should be 1802135552
which might do what you expect, but is technically undefined behavior.
The relevant reference for this second point is e.g. in §6.5 p7 in N1570, the latest draft for C11:
An object shall have its stored value accessed only by an lvalue expression that has one of
the following types:
— a type compatible with the effective type of the object,
— a qualified version of a type compatible with the effective type of the object,
— a type that is the signed or unsigned type corresponding to the effective type of the
object,
— a type that is the signed or unsigned type corresponding to a qualified version of the
effective type of the object,
— an aggregate or union type that includes one of the aforementioned types among its
members (including, recursively, a member of a subaggregate or contained union), or
— a character type.
The reasoning for this very strict rule is for example that it enables compilers to do optimizations based on the assumption that two pointers of different types (except char *) can never alias. Other reasons include alignment restrictions on some platforms.
Even if you fix your code to cast pointer to correct type (like int *) before dereferencing it, you might have problems with alignment. For example on some architectures you simply can not read an 4-byte int if it is not aligned to 4-byte word boundary.
A solution which would definitely work is to use something like this:
int result;
memcpy(&result, pointer, sizeof(result));
UPDATE:
in the updated code in the question
uint16_t value1 = *(uint16_t*)pointer;
exactly violates strict aliasing. It's invalid code.
For more details, read the rest of the answer.
Initial version:
Technically, you are not allowed to dereference a void pointer in first place.
Quoting C11, chapter §6.5.3.2
[...] If the operand points to a function, the result is
a function designator; if it points to an object, the result is an lvalue designating the
object. If the operand has type ‘‘pointer to type’’, the result has type ‘‘type’’. [...]
but, a void is a forever-incomplete type, so the storage size is not known, hence the dereference is not possible.
A gcc extension allows you to dereference the void pointer and perform arithmatic operation on them, considering it as alias for a char pointer, but better, do not reply on this. Please cast the pointer to either a character type or the actual type (or compatible) and then, go ahead with dereference.
That said, if you cast the pointer itself to some other type than a character type or an incompatible type with the original pointer, you'll violate strict aliasing rule.
As mentioned in chapter §6.5,
An object shall have its stored value accessed only by an lvalue expression that has one of
the following types
— a type compatible with the effective type of the object,
— a qualified version of a type compatible with the effective type of the object,
— a type that is the signed or unsigned type corresponding to the effective type of the
object,
— a type that is the signed or unsigned type corresponding to a qualified version of the
effective type of the object,
— an aggregate or union type that includes one of the aforementioned types among its
members (including, recursively, a member of a subaggregate or contained union), or
— a character type.
and, chapter §6.3.2.3
[....] When a pointer to an object is converted to a pointer to a character type,
the result points to the lowest addressed byte of the object. Successive increments of the
result, up to the size of the object, yield pointers to the remaining bytes of the object.

Can a char array be used with any data type?

The malloc() function returns a pointer of type void*. It allocates memory in bytes according to the size_t value passed as argument to it. The resulting allocation is raw bytes which can be used with any data type in C(without casting).
Can an array with type char declared within a function that returns void *, be used with any data type like the resulting allocation of malloc?
For example,
#include <stdio.h>
void *Stat_Mem();
int main(void)
{
//size : 10 * sizeof(int)
int buf[] = { 1,2,3,4,5,6,7,8,9,10 };
int *p = Stat_Mem();
memcpy(p, buf, sizeof(buf));
for (int n = 0; n < 10; n++) {
printf("%d ", p[n]);
}
putchar('\n');
return 0;
}
void *Stat_Mem()
{
static char Array[128];
return Array;
}
The declared type of the static object Array is char. The effective type of this object is it's declared type. The effective type of a static object cannot be changed, thus for the remainder of the program the effective type of Array is char.
If you try to access the value of an object with a type that is not compatible with, or not on this list1, the behavior is undefined.
Your code tries to access the stored value of Array using the type int. This type is not compatible with the type char and is not on the list of exceptions, so the behavior is undefined when you read the array using the int pointer p:
printf("%d ", p[n]);
1 (Quoted from: ISO:IEC 9899:201X 6.5 Expressions 7 )
An object shall have its stored value accessed only by an lvalue
expression that has one of the following types:
— a type
compatible with the effective type of the object,
— a qualified
version of a type compatible with the effective type of the object,
— a type that is the signed or unsigned type corresponding to the
effective type of the object,
— a type that is the signed or unsigned
type corresponding to a qualified version of the effective type of the
object,
— an aggregate or union type that includes one of the
aforementioned types among its members (including, recursively, a
member of a subaggregate or contained union), or
— a character type.
No you cannot use an arbitrary byte array for an arbitrary type because of possible alignment problems. The standard says in 6.3.2.3 Conversions/Pointers (emphasize mine):
A pointer to an object or incomplete type may be converted to a pointer to a different
object or incomplete type. If the resulting pointer is not correctly aligned for the
pointed-to type, the behavior is undefined. Otherwise, when converted back again, the
result shall compare equal to the original pointer.
As a char as the smallest alignment requirement, you cannot make sure that your char array will be correctly aligned for any other type. That is why malloc guarantees that a buffer obtained by malloc (even if it is a void *) has the largest possible alignement requirement to be able to accept any other type.
I think that
union {
char buf[128];
long long i;
void * p;
long double f;
};
should have correct alignment for any type as it is compatible with largest basic types (as defined in 6.2.5 Types). I am pretty sure that it will work for all common implementations (gcc, clang, msvc, ...) but unfortunately I could not find any confirmation that the standard allows it. Essentially because of the strict aliasing rule as defined in 6.5 Expression §7:
An object shall have its stored value accessed only by an lvalue expression that has one of
the following types:
a type compatible with the effective type of the object,
a qualified version of a type compatible with the effective type of the object,
a type that is the signed or unsigned type corresponding to the effective type of the
object,
a type that is the signed or unsigned type corresponding to a qualified version of the
effective type of the object,
an aggregate or union type that includes one of the aforementioned types among its
members (including, recursively, a member of a subaggregate or contained union), or
a character type.
So IMHO there is not portable and standard conformant way to build a custom allocator not using malloc.
If one reads the rationale of the C89 Standard, the only reason that the type- aliasing rules exist is to avoid requiring compilers to make "worst-case aliasing assumptions". The given example was:
int a;
void f( double * b )
{
a = 1;
*b = 2.0;
g(a);
}
If program creates a "char" array within a union containing something whose alignment would be suitable for any type, takes the address thereof, and never accesses the storage of that structure except through the resulting pointer, there should be no reason why aliasing rules should cause any difficulty.
It's worthwhile to note that the authors of the Standard recognized that an implementation could be simultaneously compliant but useless; see the rationale for C89 2.2.4.1:
While a deficient implementation could probably contrive a program that meets this requirement, yet still succeed in being useless, the Committee felt that such ingenuity would probably require more work than making something useful. The sense of the Committee is that implementors should not construe the translation limits as the values of hard-wired parameters, but rather as a set of criteria by which an implementation will be judged.
While that particular statement is made with regard to implementation limits, the only way to interpret the C89 as being even remotely compatible with the C dialects that preceded it is to regard it as applying more broadly as well: the Standard doesn't try to exhaustively specify everything that a program should be able to do, but relies upon compiler writers' exercising some common sense.
Use of a character-type array as a backing store of any type, assuming one ensures alignment issues are taken care of, shouldn't cause any problems with a non-obtusely written compiler. The Standard didn't mandate that compiler writers allow such things because they saw no reason to expect them to do otherwise. Unfortunately, they failed to foresee the path the language would take in the 21st Century.

Pass integer address in method expecting unsigned char pointer

Recently I had code (in C) where I passed the address of an int to a function expecting a pointer to unsigned char. Is this not valid? Is this UB or what?
e.g.,
void f(unsigned char*p)
{
// do something
}
// Call it somewhere
int x = 0; // actually it was uint32_t if it makes difference
f(&x);
I did get a warning though ... Compiled in Xcode
int * and unsigned char * are not considered compatible types, so implicit conversion will issue a diagnostic. However, the standard does allow explicit casting between different pointers, subject to two rules (C11 section 6.3.2.3):
Converting a type "pointer to A" to type "pointer to B" and back to "pointer to A" shall result in the same original pointer. (i.e., if p is of type int *, then (int *)(double *)p will yield p)
Converting any pointer to a char * will point to the lowest-addressable byte of the object.
So, in your case, an explicit (unsigned char *) cast will yield a conforming program without any undefined behavior.
The cast is required, see C11 (n1570) 6.5.2.2 p.2:
[…] Each argument shall have a type such that its value may be assigned to an object with the unqualified version of the type of its corresponding parameter.
This refers to the rules for assignment, the relevant part is (ibid. 6.5.16.1 p.1)
One of the following shall hold:
[…]
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.
[…]
And unsigned char isn’t compatible to int.
These rules both appear in a “constraint” section, where “shall” means that the compiler has to give a “diagnostic message” (cf. C11 5.1.1.3) and may stop compiling (or whatever, everything beyond that diagnostic is, strictly speaking, out of the scope of the C standard). Your code is an example of a constraint violation.
Other examples of constraint violations are calling a (prototyped and non-variadic) function with the wrong number of arguments, using bitwise operators on doubles, or redeclaring an identifier with an incompatible type in the same scope, ibid. 5.1.1.3 p.2:
Example
An implementation shall issue a diagnostic for the translation unit:
char i;
int i;
because in those cases where wording in this International Standard describes the behavior for a construct as being both a constraint error and resulting in undefined behavior, the constraint error shall be diagnosed.
Syntax violations are treated equally.
So, strictly speaking, your program is as invalid as
int foo(int);
int main() {
It's my birthday!
foo(0.5 ^ 42, 12);
}
which a conforming implementation very well may compile, maybe to a program having undefined behavior, as long as it gives at least one diagnostic (e.g. a warning).
For e.g. gcc, a warning is a diagnostic (you can turn syntax and constraint violations into errors with -pedantic-errors).
The term ill-formed may be used to refer to either a syntax or a constraint violation, the C standard doesn't use this term, but cf. C++11 (n3242):
1.3.9
ill-formed program
program that is not well formed
1.3.26
well-formed program
C++ program constructed according to the syntax rules, diagnosable semantic rules, and the One Definition Rule.
The language-lawyer attitude aside, your code will probably always either be not compiled at all (which should be reason enough to do the cast), or show the expected behavior.
C11, §6.5.2.2:
2 Each argument shall have a type such that its value may be assigned to an object with the unqualified version of the type of its corresponding parameter.
§6.5.16.1 describes assignment in terms of a list of constraints, including
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
int and unsigned char are not compatible types, so the program is not well-formed and the Standard doesn't even guarantee that it will compile.
Although some would say "it is undefined behavior according to the standard", here is what happens de-facto (answering by an example):
Safe:
void f(char* p)
{
char r, w = 0;
r = p[0]; // read access
p[0] = w; // write access
}
...
int x = 0;
f((char*)&x); // the casting is just in order to emit the compilation warning
This code is safe as long as you access memory with p[i], where 0 <= i <= sizeof(int)-1.
Unsafe:
void f(int* p)
{
int r, w = 0;
r = p[0]; // read access
p[0] = w; // write access
}
...
char x[sizeof(int)] = {0};
f((int*)&x); // the casting is just in order to emit the compilation warning
This code is unsafe because although the allocated variable is large enough to accommodate an int, its address in memory is not necessarily a multiple of sizeof(int). As a result, unless the compiler (as well as the underlying HW architecture) supports unaligned load/store operations, a memory access violation will occur during runtime if the address of this variable in memory is indeed not properly aligned.

Resources