I'm working with this large open source C library, and I frequently find casts between types that look like this:
unsigned char *str2 = *(unsigned char **) &str;
unsigned char *str2 = (unsigned char *) str;
void *q = *(void **)(&p[i]);
unsigned int rgba = *((unsigned int *)color);
return cbz_strnatcmp(*(const char **)a, *(const char **)b);
//both parameters are expected to be const char*
Each of the two alternatives presented involves converting a pointer of one type to a different pointer type via a cast. This is permitted, including in C89. In the original code, the converted pointer is explicitly dereferenced; in the revised code it is to be assumed that the pointer will be dereferenced elsewhere. In these regards, the two variations perform exactly the same kinds of (allowed) behavior.
There is one technical difference, however: the first alternative causes a value of type
char * to be accessed via an lvalue of type
unsigned char * when the value of the initialization expression is read so as to be assigned to
str2. Those two are not "compatible types" in the standard's sense of that term, nor is the latter an unsigned type corresponding to the former (pointers themselves don't have signedness), nor is the latter a character type or differently-qualifed version of the former, or a union type. The access therefore violates the provision of the standard colloquially known as the "strict aliasing rule".
The second alternative also converts between incompatible pointer types, but the subsequent accesses it provides for (and that the first alternative also provides for) are allowed by the strict aliasing rule, both because character types can alias anything, and because
unsigned char is the unsigned type corresponding to
char, and aliasing between such corresponding types is permitted.
In practice, it's unlikely that any production-ready compiler would do anything with the original code other than what is apparently expected, but your revised code is both cleaner and more correct.