Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When are type-punned pointers safe in practice?

A colleague of mine is working on C++ code that works with binary data arrays a lot. In certain places, he has code like

char *bytes = ...
T *p = (T*) bytes;
T v = p[i]; // UB

Here, T can be sometimes short or int (assume 16 and 32 bit respectively).

Now, unlike my colleague, I belong to the "no UB if at all possible" camp, while he is more along the lines of "if it works, it's OK". I am having a hard time trying to convince him otherwise.

Given that:

  1. bytes really come from somewhere outside this compilation unit, being read from some binary file.

  2. It's safe to assume that array really contains integers in the native endianness.

In practice, given mainstream C++ compilers like MSVC 2017 and gcc 4.8, and Intel x64 hardware, is such a thing really safe? I know it wouldn't be if T was, say, float (got bitten by it in the past).

like image 269
Sergei Tachenov Avatar asked Jun 15 '18 06:06

Sergei Tachenov


People also ask

What is a type Punned pointer?

A form of pointer aliasing where two pointers and refer to the same location in memory but represent that location as different types. The compiler will treat both "puns" as unrelated pointers. Type punning has the potential to cause dependency problems for any data accessed through both pointers.

Is type punning safe?

The only safe manner of using type punning is with unsigned char or well unsigned char arrays (because we know that members of array objects are strictly contiguous and there is not any padding bytes when their size is computed with sizeof() ).


2 Answers

char* can alias other entities without breaking strict aliasing rule.

Your code would be UB only if originally p + i wasn't a T originally.

char* byte = (char*) floats;
int *p = (int*) bytes;
int v = p[i]; // UB

but

char* byte = (char*) floats;
float *p = (float*) bytes;
float v = p[i]; // OK

If origin of byte is "unknown", compiler cannot benefit of UB for optimization and should assume we are in valid case and generate code according. But how do you guaranty it is unknown ? Even outside the TU, something like Link-Time Optimization might allow to provide the hidden information.

like image 72
Jarod42 Avatar answered Oct 07 '22 02:10

Jarod42


Type-punned pointers are safe if one uses a construct which is recognized by the particular compiler one is using [i.e. any compiler that is configured support quality semantics if one is using straightforward constructs; neither gcc nor clang support quality semantics qualifies with optimizations are enabled, however, unless one uses -fno-strict-aliasing]. The authors of C89 were certainly aware that many applications required the use of various type-punning constructs beyond those mandated by the Standard, but thought the question of which constructs to recognize was best left as a quality-of-implementation issue. Given something like:

struct s1 { int objectClass; };
struct s2 { int objectClass; double x,y; };
struct s3 { int objectClass; char someData[32]; };

int getObjectClass(void *p) { return ((struct s1*)p)->objectClass; }

I think the authors of the Standard would have intended that the function be usable to read field objectClass of any of those structures [that is pretty much the whole purpose of the Common Initial Sequence rule] but there would be many ways by which compilers might achieve that. Some might recognize function calls as barriers to type-based aliasing analysis, while others might treat pointer casts in such a fashion. Most programs that use type punning would do several things that compilers might interpret as indications to be cautious with optimizations, so there was no particular need for a compiler to recognize any particular one of them. Further, since the authors of the Standard made no effort to forbid implementations that are "conforming" but are of such low-quality implementations as to be useless, there was no need to forbid compilers that somehow managed not to see any of the indications that storage might be used in interesting ways.

Unfortunately, for whatever reason, there hasn't been any effort by compiler vendors to find easy ways of recognizing common type-punning situations without needlessly impairing optimizations. While handling most cases would be fairly easy if compiler writers hadn't adopted designs that filter out the clearest and most useful evidence before applying optimization logic, both the designs of gcc and clang--and the mentalities of their maintainers--have evolved to oppose such a concept.

As far as I'm concerned, there is no reason why any "quality" implementation should have any trouble recognizing type punning in situations where all operations upon a byte of storage using a pointer converted to a pointer-to-PODS, or anything derived from that pointer, occur before the first time any of the following occurs:

  1. That byte is accessed in conflicting fashion via means not derived from that pointer.

  2. A pointer or reference is formed which will be used sometime in future to access that byte in conflicting fashion, or derive another that will.

  3. Execution enters a function which will do one of the above before it exits.

  4. Execution reaches the start of a bona fide loop [not, e.g. a do{...}while(0);] which will do one of the above before it exits.

A decently-designed compiler should have no problem recognizing those cases while still performing the vast majority of useful optimizations. Further, recognizing aliasing in such cases would be simpler and easier than trying to recognize it only in the cases mandated by the Standard. For those reasons, compilers that can't handle at least the above cases should be viewed as falling in the category of implementations that are of such low quality that the authors of the Standard didn't particularly want to allow, but saw no reason to forbid. Unfortunately, neither gcc nor clang offer any options to behave reasonably except by requiring that they disable type-based aliasing altogether. Unfortunately, the authors of gcc and clang would rather deride as "broken" any code needing features beyond what the Standard requires, than attempt a useful blend of optimization and semantics.

Incidentally, neither gcc nor clang should be relied upon to properly handle any situation in which storage that has been used as one type is later used as another, even when the Standard would require them to do so. Given something like:

union { struct s1 v1; struct s2 v2} unionArr[100];
void test(int i)
{
    int test = unionArr[i].v2.objectClass;
    unionArr[i].v1.objectClass = test;
}

Both clang and gcc will treat it as a no-op even if it is executed between code which writes unionArr[i].v2.objectClass and code which happens to reads member v1.objectClass of the same union object, thus causing them to ignore the possibility that the write to unionArr[i].v2.objectClass might affect v1.objectClass.

like image 44
supercat Avatar answered Oct 07 '22 00:10

supercat