C's treatment of void * is not broken

Christopher Bazley, July 2025

A serious hole?

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):

A void* cannot be assigned to anything without a cast. Allowing implicit conversions of void* 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 a long int or a double to an int, or a pointer to void to 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.

Importance of pointers to void

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!

Counterargument

Conversions to and from void * can be modeled as subtype polymorphism:

  1. void * subsumes int * because void * can represent pointers to objects of types double, char or any other type as well as pointers to int.
  2. 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.)

Rationalisation

Aside from the obvious practical benefits of permitting conversions from void * to other pointer types, I believe this laxity can be rationalised as follows:

  1. A pointer to a qualified type such as 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.
  2. 'Cannot be dereferenced' is not a property that programmers care about: it doesn't pertain to objects at the point of definition, and it is not added to restrict usage. It's just a side-effect of the fact that the type of object that 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.