During a recent discussion, the question came up of whether the C programming language's handling of conversions from type void * to other pointer types is a large hole in type safety.
Stroustrup made a similar claim in "The Design and Evolution of C++" (1994):
Avoid*cannot be assigned to anything without a cast. Allowing implicit conversions ofvoid*to other pointer types would open a serious hole in the type system. One might make a special case for(void*)0, but special cases should only be admitted in dire need.
This is part of his explanation of why the macro NULL cannot be defined as (void*)0 in C++, unlike in C where that is the most sensible definition. In other words, he created a problem for himself, which later had to be solved by the invention of nullptr.
In C, a pointer to the type void can be implicitly converted to a pointer to any other type:
void bar(int *j)
{
(void)j;
}
void foo(void *i)
{
bar(i);
}
This follows from the rules for simple assignment (6.5.17.2) in the ISO C standard, which permit conversions in both directions (to and from pointer to void):
the left operand has atomic, qualified, or unqualified pointer type, and (considering the type the left operand would have after lvalue conversion) 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 operand has all the qualifiers of the type pointed to by the right operand;
We can observe similar behaviour when using the Any type annotation in Python:
from typing import Any
def bar(j:int) -> None:
pass
def foo(i:Any) -> None:
bar(i)
pass
Is this a "serious hole in the type system" - or is it just a well-designed type system functioning as intended?
In contrast, C++ forbids an implicit conversion from type void * to type int *:
<source>: In function 'void foo(void*)':
<source>:8:9: error: invalid conversion from 'void*' to 'int*' [-fpermissive]
8 | bar(i);
| ^
| |
| void*
<source>:1:15: note: initializing argument 1 of 'void bar(int*)'
1 | void bar(int *j)
| ~~~~~^
Compiler returned: 1
Confusingly, Annex I of the ISO C standard claims that "...common situations where an implementation may generate a warning" include:
An implicit narrowing conversion is encountered, such as the assignment of along intor adoubleto anint, or a pointer tovoidto a pointer to any type other than a character type (6.3).
This annex is informative rather than normative, and I have never yet seen a C compiler generate a warning when a pointer to void is assigned to a pointer to any other type. I consider that to be a good thing because I do not think that warnings should be emitted about code that cannot reasonably be improved.
The type void * is at the heart of support for polymorphism in C, because it is the main mechanism by which programmers can specify a single interface to objects of different types.
I do not want to have to add a cast to every instance of
EditWin *const edit_win = handle;
in code such as the following event handler (and many similar function definitions in this and other source files):
static int scroll_request(int const event_code, WimpPollBlock *const event,
IdBlock *const id_block, void *const handle)
{
/* Respond to scroll request events */
NOT_USED(event_code);
EditWin *const edit_win = handle;
I believe that requiring casts would harm readability, undermine type-safety, and benefit no one.
Consider what is lost by requiring a cast:
The operand of the cast could be a pointer to a qualified type such as const void *. The cast discards that information, which could be an unintended side-effect. It may not be clear whether discarding a qualifier is intentional. The operand of the cast might not be a pointer at all, e.g. it might have type uintptr_t or some other type that happens to be convertible to a pointer without provoking a diagnostic message. The cast discards that information, allowing such accidents. It may not be clear whether converting an integer to a pointer is intentional.
A mechanism for polymorphism that is too cumbersome for anyone to bother with (or no less cumbersome than the frowned-upon alternatives) is to all intents and purposes useless. That doesn't matter for C++ because they want to discourage use of void *, but it does matter a great deal for C.
Imagine that the type void * did not exist. The scroll_request function could still be defined, but its final parameter could have type unsigned char * instead of type void *. From a usability point of view, there would be little difference: in C++, both types require a cast to convert them to some other type!
Conversions to and from void * can be modeled as subtype polymorphism:
void * subsumes int * because void * can represent pointers to objects of types double, char or any other type as well as pointers to int.
int * is substitutable for void * because an expression of type int * can be assigned to an lvalue or parameter of type void *.
According to this model, void * is a superclass of int *: converting from int * to void * is an upcast, and converting from void * to int * is a downcast.
Conventionally, an explicit downcast is needed to remove restrictions on usage that cannot be discarded implicitly. If expressions of type void * could only be assigned to lvalues or parameters of type void * (as in C++), then the 'cannot be dereferenced' property of the pointer would not be lost.
Let's contrast my preferred behaviour for conversions from void * with the actual behaviour of conversions from const int *.
void * can represent all values of type int *, and the type of the object initialized by the second declaration is as explicit as any cast:
void *vp;
int *ip = vp; // okay
Likewise, const int * can represent all values of type int *, and the type of the object initialized by the second declaration is as explicit as any cast:
const int *cip;
int *ip = cip; // constraint violation
If it is desirable to allow implicit downcasts from void * to int *, why not allow implicit downcasts from const int * to int *?
(This question is rhetorical: implicitly discarding a const qualifier in the initialisation of ip would remove any guard against later assigning a value to *ip, which would have undefined behaviour if ip pointed to an object defined with a const-qualified type.)
Aside from the obvious practical benefits of permitting conversions from void * to other pointer types, I believe this laxity can be rationalised as follows:
const int can be used without casting (e.g., by dereferencing the pointer), but that is not true of a pointer to void. With rare exceptions, a pointer is only converted to void * in the expectation that it will eventually be converted back to its real type; it is not converted to void * to prevent it from being dereferenced.
void * points to is unknown.
Maybe I'm wrong, but I believe that the concept of any-type (as expressed via void *) is orthogonal to the concept of qualified type or subtype. Python's treatment of the type annotation Any supports this point of view.