Question

Widespread use of non-portable `void **` casts

I believe the following code is well-defined according to the C standard:

void f(void **);
void *p;
f((void **)&p);

But what if p isn't a pointer to void, say:

void f(void **);
double *p;
f((void **)&p);

According to the C FAQ, it is not portable. However, it does not cause my compiler to emit any warnings, and searching well known C codebases yields many instances of such non-void *-casts, for example Linux, CPython, and COM all do it. The answers of the most closely related SO question that I could find make no mention of this being strange or nonstandard. So, is this usage of void ** really non-portable? And if so why does it seem to be common practice?

 3  103  3
1 Jan 1970

Solution

 5

It is undefined behaviour to dereference the resulting void **, so you might as well use a void *.

double *p;
void **vp = (void **)p;  // UB if it violates alignment restrictions.
*vp = NULL;              // UB

If void * and double * have different alignment restrictions, the behaviour is undefined. It is otherwise allowed.

C17 §6.3.2.3 ¶7 A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned 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.

So the cast itself should be safe in most environments. But what if you dereference the void **?

This is a "strict aliasing violation" and thus undefined behaviour.

C17 §6.5 ¶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.
2024-07-25
ikegami

Solution

 1

Actually the cast is wrong and non-portable but most compilers can't even detect the problem.

Suppose we have a word-aligned architecture but double is 80 bits long and uses partial words. (This is far more likely with char or long double than double I digress.)

We may end up with sizeof(int *) == 4 but sizeof(double *) == 8; therefore sizeof(void *) must also be 8; however sizeof (void **) can easily be 4.

This results in function f shearing part of the pointer off and the code not working. A compiler for a platform that actually exhibits this behavior would successfully warn against this; however most compilers only target architectures for which all pointer sizes are the same and don't have a way of detecting the problem.

Platforms that behave like this are mostly gone and nobody outside of the embedded world cares. The only one I have ever encountered was nasty and completely nonportable because const void * and void * were different sizes due to the fact that ROM was bigger than the CPU's word size but RAM was not.

While this particular sample tends to work; most examples of this form are also unstable due to violating the strict aliasing rule.

2024-07-25
Joshua