2

To my understanding, void ** is not compatible with int **. I expect the code below to emit a warning for a violation of the strict aliasing rule, on the line where ptrs is cast to void ** and dereferenced. I have replaced int with MyType in this snippet to illustrate that this would be a problem for any type.

#include <stdlib.h>

typedef int MyType;
const MyType mytype_0 = 0;

int main()
{
    MyType **ptrs = malloc(10 * sizeof(MyType *));
    for (size_t i=0; i<10; i++) {
        ptrs[i] = malloc(sizeof(MyType));
        *ptrs[i] = mytype_0;
    }

    void *void_ptr = ((void **)ptrs)[3]; /* violating strict aliasing rule? */
    MyType *ptr = (MyType *)void_ptr;
    MyType val = *ptr;
}

With Wstrict-aliasing=1, the warning dereferencing type-punned pointer might break strict-aliasing rules is emitted. However, that warning goes away when setting the warning level to 2 or 3. According to the GCC documentation on this warning level,

Higher levels correspond to higher accuracy (fewer false positives)

which seems an indication that my understanding is incorrect.

Reinier Torenbeek
  • 16,669
  • 7
  • 46
  • 69
  • 1
    Isn't `ptrs[3]` a `MyType*`? – Jim Rhodes Mar 23 '23 at 20:06
  • @JimRhodes yes but `ptrs` gets casted to `void **` before it is indexed by `[3]`. I have added parentheses around it to make sure it does what I intended, I don't think that was needed though. – Reinier Torenbeek Mar 23 '23 at 20:17
  • @ReinierTorenbeek: `(void **)ptrs[3]` is parsed as `(void **) (ptrs[3])`. The parentheses are needed if you want `((void **) ptrs)[3]`. – Eric Postpischil Mar 23 '23 at 20:19
  • @EricPostpischil thanks, my bad, I should have looked that up. I don't use constructs like these too often myself – Reinier Torenbeek Mar 23 '23 at 20:21
  • 1
    Your understanding that void* and int* are not compatible, and that the code given contains a strict aliasing violation is correct (https://stackoverflow.com/questions/69896206/is-void-an-exception-to-strict-aliasing-rules), as to why gcc does not emit a warning in this particular case, I don't know. – ChrisB Mar 23 '23 at 20:29
  • Your conception of `void*` is incorrect. You don't need `void**` you simply need `void*` -- it can point to any type of pointer (`void**` would be a *pointer-to-pointer-to* `void`). In your example, all you need is `void *void_ptr = ptrs[3]; MyType *ptr = void_ptr; MyType val = *ptr;` In C a `void*` can be assigned to/from any other pointer type without a cast. The key to remember is with `void*` there is no *type*, so there is no ability to perform any operation that relies on pointer arithmetic which is dependent on type/type-size. – David C. Rankin Mar 23 '23 at 22:04
  • @DavidC.Rankin OK, I overlooked that in this example, thanks. In reality, the code I am analyzing also does a cast of `ptrs` to a `void *` after its elements have been filled, at which point I can no longer simply do `[3]` on it. My simplification omitted that. – Reinier Torenbeek Mar 24 '23 at 00:44
  • @ReinierTorenbeek - correct, because after the cast to `void *` no type or type-size information exists for the dereference associated with indexing `[3]`, because recall `ptrs[3]` (or generic `array[3]`) is the same as `*(ptrs + 3)` and without a type-size, `ptrs + 3` cannot be computed. – David C. Rankin Mar 24 '23 at 04:02
  • These warning levels for strict aliasing have never worked, they've been broken for decades. Don't trust them or rely on them. – Lundin Mar 24 '23 at 07:33
  • @Lundin that is pretty firm language. If that is true, I wonder why this is not at least mentioned in the `gcc` manual, to avoid confusing people. – Reinier Torenbeek Mar 24 '23 at 17:38
  • The gcc manual was always poor, just as gcc's handling of strict aliasing was always poor. For example just now I tried `int x[4] = {1,2,3,4}; double res = func(x);` where `func` is a function expecting a `double`. With `-Wstrict-aliasing=1 -O3` I get the diagnostic about incompatible pointer types but not the one about strict aliasing. If I change to `func((double*)x);` then I get the strict aliasing warning. And yet in the former case gcc still decided to create an executable with UB strict aliasing violations, where it will go completely loco speculatively abusing UB... – Lundin Mar 27 '23 at 09:00
  • This is _not_ a quality compiler implementation: https://godbolt.org/z/3714oavYn. Yes it is UB but the compiler didn't have to produce utter broken code because of it. It could have read the actual memory to update what was stored there. – Lundin Mar 27 '23 at 09:01

2 Answers2

2

Are void** and int** compatible types?

Your understanding is correct: they are not "compatible" types as the language spec defines that.

I expect the code below to emit a warning for a violation of the strict aliasing rule, on the line where ptrs is cast to void ** and dereferenced.

Why?

Yes, you have a violation of the strict aliasing rule. However, the SAR is a semantic rule, not a language constraint, so the language specification does not require conforming implementations to diagnose violations (much less non-conforming ones). Moreover, although in the example code, the violation could be detected at compilation time, that's not universally true of SAR violations.

Furthermore, in this particular case, the program will likely work just fine on many systems unless the compiler is wantonly perverse, because typically, all object pointers have the same representation.

With Wstrict-aliasing=1, the warning dereferencing type-punned pointer might break strict-aliasing rules is emitted.

Which is perfectly reasonable, since the strict-aliasing rule is in fact violated.

However, that warning goes away when setting the warning level to 2 or 3, which seems an indication that my understanding is incorrect.

No compiler is perfect, and the manual says that -Wstrict-aliasing=1 has few false negatives, not none. Moreover, it does warn about your code when optimization is not enabled.

That the compiler does not emit a diagnostic is not a sound basis for concluding that there is no issue. Especially when under some conditions it in fact does diagnose.

That enabling optimization prevents the compiler from diagnosing the strict-aliasing violation most likely reflects that at least some of its optimization passes are performed prior to the strict aliasing analysis, and that they have the effect of preventing it from recognizing this particular one.

For example, I can imagine the void_ptr temporary being eliminated altogether, in conjunction with the (void **) cast being adjusted or eliminated appropriately, which would optimize the strict-aliasing violation right out of the resulting intermediate representation. That's speculative, of course, but in the end, it all comes back around to the compiler not being required to diagnose the violation.

And also, I suspect, to the particular violation in this case not presenting a problem in practice for GCC-compiled code.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • 2
    Re “Why?”: [Because GCC documents it will provide warnings when `-fstrict-aliasing` is active and `-Wstrict-aliasing=n` is given.](https://gcc.gnu.org/onlinedocs/gcc-12.2.0/gcc/Warning-Options.html#Warning-Options) – Eric Postpischil Mar 23 '23 at 21:02
  • 1
    Re “because typically, all object pointers have the same representation”: That is an invalid inference because compiler optimization may (and some do) use pointer type information (or lvalue type information generally), without regard to the fact that the representations are the same. – Eric Postpischil Mar 23 '23 at 21:04
  • @EricPostpischil, I said the program will *likely* work just fine *unless the compiler is wantonly perverse*. That's a value judgement on one hand and a pragmatic and empirically supportable analysis on the other. – John Bollinger Mar 23 '23 at 21:10
  • 1
    The bottom line is usually (6.3.2.3) "A pointer to any object type may be converted to a pointer to void and back again; the result shall compare equal to the original pointer." In order for a compiler to fulfill this requirement, the only sensible implementation is to give void pointers and all other object pointers the same representation. Even on systems with exotic pointer sizes varying in width (8/16 bit CPUs with extended addressing) then every compiler I've ever worked with picked that route too. And instead covered the exotic cases by non-standard `near`/`far` pointer qualifiers. – Lundin Mar 24 '23 at 07:46
  • Which of the following is true: “the program will likely work just fine on many systems…” or [”"Undefined" means what it says on the tin: you cannot rely on any particular behavior”](https://stackoverflow.com/a/71441194/298225)? – Eric Postpischil Mar 24 '23 at 11:40
  • Both are true, @EricPostpischil. "will likely work" is not inconsistent with "you cannot rely". – John Bollinger Mar 24 '23 at 13:15
  • @JohnBollinger thanks for the helpful answer. What did you mean by _Moreover, it does warn about your code when optimization is not enabled._ though? I tried a few combinations of compiler settings but "not enabling" optimization did not seem to make a difference with regard to the warning(s) (not) being emitted. – Reinier Torenbeek Mar 24 '23 at 17:50
  • @ReinierTorenbeek, it looks like I misread the last part of the question, taking "warning level" as "optimization level". However, I did observe in my own tests that the warning was not emitted when I compiled with optimization level `-O2` or above. (I don't remember whether I tried just `-O`.) – John Bollinger Mar 24 '23 at 17:56
  • @EricPostpischil: The latter statement is only true of compilers that are not designed to be suitable for low-level programming on systems where "behaving during translation or program execution in a documented manner characteristic of the environment" would yield useful and predictable results. – supercat Apr 04 '23 at 15:15
1

There are platforms where void* and int* have different representations and are not even the same size (void* is larger). On such platforms, casting an the address of an int* to a void** and using the latter to access the pointer would be unlikely to work.

On platforms where pointers to all kinds of objects have the same representation as void* (i.e. all commonplace platforms, and most obscure ones as well), some compilers will usefully extend the language to allow the address of any pointer to be cast to void** and dereferenced to manipulate the pointer. Even the -fstrict-aliasing modes of clang and gcc appear to support this, but I wouldn't particularly trust that future maintainers will view that as a useful feature rather than a "missed optimization" except when using -fno-strict-aliasing mode.

supercat
  • 77,689
  • 9
  • 166
  • 211