I\'ve read various posts on Stack Overflow RE: the derefercing type-punned pointer error. My understanding is that the error is essentially the compiler warning of the danger of
Although C was designed for machines which use the same representation for all pointers, the authors of the Standard wanted to make the language usable on machines that use different representations for pointers to different types of objects. Therefore, they did not require that machines which use different pointer representations for different kinds of pointers support a "pointer to any kind of pointer" type, even though many machines could do so at zero cost.
Before the Standard was written, implementations for platforms that used the same representation for all pointer types would unanimously allow a void**
to be used, at least with suitable casting, as a "pointer to any pointer". The authors of the Standard almost certainly recognized that this would be useful on platforms that supported it, but since it couldn't be universally supported they declined to mandate it. Instead, they expected that quality implementation would process such constructs as what the Rationale would describe as a "popular extension", in cases where doing so would make sense.
This code is invalid per the C Standard, so it might work in some cases, but is not necessarily portable.
The "strict aliasing rule" for accessing a value via a pointer that has been cast to a different pointer type is found in 6.5 paragraph 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.
In your *obj = NULL;
statement, the object has effective type Foo*
but is accessed by the lvalue expression *obj
with type void*
.
In 6.7.5.1 paragraph 2, we have
For two pointer types to be compatible, both shall be identically qualified and both shall be pointers to compatible types.
So void*
and Foo*
are not compatible types or compatible types with qualifiers added, and certainly don't fit any of the other options of the strict aliasing rule.
Although not the technical reason the code is invalid, it's also relevant to note section 6.2.5 paragraph 26:
A pointer to
void
shall have the same representation and alignment requirements as a pointer to a character type. Similarly, pointers to qualified or unqualified versions of compatible types shall have the same representation and alignment requirements. All pointers to structure types shall have the same representation and alignment requirements as each other. All pointers to union types shall have the same representation and alignment requirements as each other. Pointers to other types need not have the same representation or alignment requirements.
As for the differences in warnings, this is not a case where the Standard requires a diagnostic message, so it's just a matter of how good the compiler or its version is at noticing potential issues and pointing them out in a helpful way. You noticed optimization settings can make a difference. This is often because more information is internally generated about how various pieces of the program actually fit together in practice, and that extra information is therefore also available for warning checks.
A void *
is treated specially by the C standard in part because it references an incomplete type. This treatment does not extend to void **
as it does point to a complete type, specifically void *
.
The strict aliasing rules say you can't convert a pointer of one type to a pointer of another type and subsequently dereference that pointer because doing so means reinterpreting the bytes of one type as another. The only exception is when converting to a character type which allows you to read the representation of an object.
You can get around this limitation by using a function-like macro instead of a function:
#define freeFunc(obj) (free(obj), (obj) = NULL)
Which you can call like this:
freeFunc(f);
This does have a limitation however, because the above macro will evaluate obj
twice. If you're using GCC, this can be avoided with some extensions, specifically the typeof
keyword and statement expressions:
#define freeFunc(obj) ({ typeof (&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })
Dereferencing a type punned pointer is UB and you can't count on what will happen.
Different compilers generate different warnings, and for this purpose different versions of the same compiler can be considered as different compilers. This seems a better explanation for the variance you see than a dependence on the architecture.
A case which may help you understand why type punning in this case can be bad is that your function won't work on an architecture for which sizeof(Foo*) != sizeof(void*)
. That is authorized by the standard although I don't know any current one for which this is true.
A workaround would be to use a macro instead of a function.
Note that free
accepts null pointers.
On top of what the other answers have said, this is a classic anti-pattern in C, and one which should be burned with fire. It appears in:
void *
(which doesn't suffer from this issue because it involves a value conversion instead of type punning), instead returning an error flag and storing the result via a pointer-to-pointer.For another example of (1), there was a longstanding infamous case in ffmpeg/libavcodec's av_free
function. I believe it was eventually fixed with a macro or some other trick, but I'm not sure.
For (2), both cudaMalloc and posix_memalign
are examples.
In neither case does the interface inherently require invalid usage, but it strongly encourages it, and admits correct usage only with an extra temporary object of type void *
that defeats the purpose of the free-and-null-out functionality, and makes allocation awkward.
A value of type void**
is a pointer to an object of type void*
. An object of type Foo*
is not an object of type void*
.
There is an implicit conversion between values of type Foo*
and void*
. This conversion may change the representation of the value. Similarly, you can write int n = 3; double x = n;
and this has the well-defined behavior of setting x
to the value 3.0
, but double *p = (double*)&n;
has undefined behavior (and in practice will not set p
to a “pointer to 3.0
” on any common architecture).
Architectures where different types of pointers to objects have different representations are rare nowadays, but they are permitted by the C standard. There are (rare) old machines with word pointers which are addresses of a word in memory and byte pointers which are addresses of a word together with a byte offset in this word; Foo*
would be a word pointer and void*
would be a byte pointer on such architectures. There are (rare) machines with fat pointers which contain information not only about the address of the object, but also about its type, its size and its access control lists; a pointer to a definite type might have a different representation from a void*
which needs additional type information at runtime.
Such machines are rare, but permitted by the C standard. And some C compilers take advantage of the permission to treat type-punned pointers as distinct to optimize code. The risk of pointers aliasing is a major limitation to a compiler's ability to optimize code, so compilers tend to take advantage of such permissions.
A compiler is free to tell you that you're doing something wrong, or to quietly do what you didn't want, or to quietly do what you wanted. Undefined behavior allows any of these.
You can make freefunc
a macro:
#define FREE_SINGLE_REFERENCE(p) (free(p), (p) = NULL)
This comes with the usual limitations of macros: lack of type safety, p
is evaluated twice. Note that this only gives you the safety of not leaving dangling pointers around if p
was the single pointer to the freed object.