Best practices for object oriented patterns with strict aliasing and strict alignment in C
Asked Answered
K

3

10

I've been writing embedded C code for many years now, and the newer generations of compilers and optimizations have certainly gotten a lot better with respect to their ability to warn about questionable code.

However, there is at least one (very common, in my experience) use-case that continues to cause grief, wheres a common base type is shared between multiple structs. Consider this contrived example:

#include <stdio.h>

struct Base
{
    unsigned short t; /* identifies the actual structure type */
};

struct Derived1
{
    struct Base b; /* identified by t=1 */
    int i;
};

struct Derived2
{
    struct Base b; /* identified by t=2 */
    double d;
};


struct Derived1 s1 = { .b = { .t = 1 }, .i = 42 };
struct Derived2 s2 = { .b = { .t = 2 }, .d = 42.0 };

void print_val(struct Base *bp)
{
    switch(bp->t)
    {
    case 1: 
    {
        struct Derived1 *dp = (struct Derived1 *)bp;
        printf("Derived1 value=%d\n", dp->i);
        break;
    }
    case 2:
    {
        struct Derived2 *dp = (struct Derived2 *)bp;
        printf("Derived2 value=%.1lf\n", dp->d);
        break;
    }
    }
}

int main(int argc, char *argv[])
{
    struct Base *bp1, *bp2;

    bp1 = (struct Base*) &s1;
    bp2 = (struct Base*) &s2;
    
    print_val(bp1);
    print_val(bp2);

    return 0;
}

Per ISO/IEC9899, the casts within code above should be OK, as it relies on the first member of the structure sharing the same address as the containing structure. Clause 6.7.2.1-13 says so:

Within a structure object, the non-bit-field members and the units in which bit-fields
reside have addresses that increase in the order in which they are declared. A pointer to a
structure object, suitably converted, points to its initial member (or if that member is a
bit-field, then to the unit in which it resides), and vice versa. There may be unnamed
padding within a structure object, but not at its beginning.

The casts from derived to base work fine, but the cast back to the derived type within print_val() generates an alignment warning. However this is known to be safe as it is specifically the "vice versa" part of the clause above. The problem is that the compiler simply doesn't know that the we've already guaranteed that the structure is in fact an instance of the other type via other means.

When compiled with gcc version 9.3.0 (Ubuntu 20.04) using flags -std=c99 -pedantic -fstrict-aliasing -Wstrict-aliasing -Wcast-align=strict -O3 I get:

alignment-1.c: In function ‘print_val’:
alignment-1.c:30:31: warning: cast increases required alignment of target type [-Wcast-align]
   30 |         struct Derived1 *dp = (struct Derived1 *)bp;
      |                               ^
alignment-1.c:36:31: warning: cast increases required alignment of target type [-Wcast-align]
   36 |         struct Derived2 *dp = (struct Derived2 *)bp;
      |                               ^

A similar warning occurs in clang 10.

Rework 1: pointer to pointer

A method used in some circumstances to avoid the alignment warning (when the pointer is known to be aligned, as is the case here) is to use an intermediate pointer-to-pointer. For instance:

struct Derived1 *dp = *((struct Derived1 **)&bp);

However this just trades the alignment warning for a strict aliasing warning, at least on gcc:

alignment-1a.c: In function ‘print_val’:
alignment-1a.c:30:33: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
   30 |         struct Derived1 *dp = *((struct Derived1 **)&bp);
      |                                ~^~~~~~~~~~~~~~~~~~~~~~~~

Same is true if cast done as an lvalue, that is: *((struct Base **)&dp) = bp; also warns in gcc.

Notably, only gcc complains about this one - clang 10 seems to accept this either way without warning, but I'm not sure if that's intentional or not.

Rework 2: union of structures

Another way to rework this code is using a union. So the print_val() function can be rewritten something like:

void print_val(struct Base *bp)
{
    union Ptr
    {
        struct Base b;
        struct Derived1 d1;
        struct Derived2 d2;
    } *u;

    u = (union Ptr *)bp;
...

The various structures can be accessed using the union. While this works fine, the cast to a union is still flagged as violating alignment rules, just like the original example.

alignment-2.c:33:9: warning: cast from 'struct Base *' to 'union Ptr *' increases required alignment from 2 to 8 [-Wcast-align]
    u = (union Ptr *)bp;
        ^~~~~~~~~~~~~~~
1 warning generated.

Rework 3: union of pointers

Rewriting the function as follows compiles cleanly in both gcc and clang:

void print_val(struct Base *bp)
{
    union Ptr
    {
        struct Base *bp;
        struct Derived1 *d1p;
        struct Derived2 *d2p;
    } u;

    u.bp = bp;

    switch(u.bp->t)
    {
    case 1:
    {
        printf("Derived1 value=%d\n", u.d1p->i);
        break;
    }
    case 2:
    {
        printf("Derived2 value=%.1lf\n", u.d2p->d);
        break;
    }
    }
}

There seems to be conflicting information out there as to whether this is truly valid. In particular, an older aliasing write-up at https://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html specifically calls out a similar construct as being invalid (see Casting through a union (3) in that link).

In my understanding, because pointer members of the union all share a common base type, this doesn't actually violate any aliasing rules, because all accesses to struct Base will in fact be done via an object of type struct Base - whether by dereferencing the bp union member or by accessing the b member object of the d1p or d2p. Either way it is accessing the member correctly via an object of type struct Base - so as far as I can tell, there is no alias.

Specific Questions:

  1. Is the union-of-pointers suggested in rework 3 a portable, safe, standards compliant, acceptable method of doing this?
  2. If not, is there a method that is fully portable and standards compliant, and does not rely on any platform-defined/compiler-specific behavior or options?

It seems to me that since this pattern is fairly common in C code (in the absence of true OO constructs like in C++) that it should be more straightforward to do this in a portable way without getting warnings in one form or another.

Thanks in advance!

Update:

Using an intermediate void* may be the "right" way to do this:

struct Derived1 *dp = (void*)bp;

This certainly works but it really allows any conversion at all, regardless of type compatibility (I suppose the weaker type system of C is fundamentally to blame for this, what I really want is an approximation of C++ and the static_cast<> operator)

However, my fundamental question (misunderstanding?) about strict aliasing rules remains:

Why does using a union type and/or pointer-to-pointer violate strict aliasing rules? In other words what is fundamentally different between what is done in main (taking address of b member) and what is done in print_val() other than the direction of the conversion? Both yield the same situation - two pointers that point to the same memory, which are different struct types - a struct Base* and a struct Derived1*.

It would seem to me that if this were violating strict aliasing rules in any way, the introduction of an intermediate void* cast would not change the fundamental problem.

Knap answered 1/3, 2021 at 18:41 Comment(0)
F
6

You can avoid the compiler warning by casting to void * first:

struct Derived1 *dp = (struct Derived1 *) (void *) bp;

(After the cast to void *, the conversion to struct Derived1 * is automatic in the above declaration, so you could remove the cast.)

The methods of using a pointer-to-a-pointer or a union to reinterpret a pointer are not correct; they violate the aliasing rule, as a struct Derived1 * and a struct Base * are not suitable types for aliasing each other. Do not use those methods.

(Due to C 2018 6.2.6.1 28, which says “… All pointers to structure types shall have the same representation and alignment requirements as each other…,” an argument can be made that reinterpreting one pointer-to-a-structure as another through a union is supported by the C standard. Footnote 49 says “The same representation and alignment requirements are meant to imply interchangeability as arguments to functions, return values from functions, and members of unions.” At best, however, this is a kludge in the C standard and should be avoided when possible.)

Why does using a union type and/or pointer-to-pointer violate strict aliasing rules? In other words what is fundamentally different between what is done in main (taking address of b member) and what is done in print_val() other than the direction of the conversion? Both yield the same situation - two pointers that point to the same memory, which are different struct types - a struct Base* and a struct Derived1*.

It would seem to me that if this were violating strict aliasing rules in any way, the introduction of an intermediate void* cast would not change the fundamental problem.

The strict aliasing violation occurs in aliasing the pointer, not in aliasing the structure.

If you have a struct Derived1 *dp or a struct Base *bp and you use it to access a place in memory where there actually is a struct Derived1 or, respectively, a struct Base, then there is no aliasing violation because you are accessing an object through an lvalue of its type, which is allowed by the aliasing rule.

However, this question suggested aliasing a pointer. In *((struct Derived1 **)&bp);, &bp is the location where there is a struct Base *. This address of a struct Base * is converted to the address of a struct Derived1 **, and then * forms an lvalue of type struct Derived1 *. The expression is then used to access a struct Base * using a type of struct Derived1 *. There is no match for that in the aliasing rule; none of the types it lists for accessing a struct Base * are a struct Derived1 *.

Fame answered 1/3, 2021 at 19:10 Comment(8)
Yeah, I should have included the intermediate void* option in my OP. The only reason I feel this is less-than-ideal is because it is really "anything goes" -- I'd like to get a warning if casting between types that are truly incompatible. But a void* basically means anything goes.Knap
I suppose I don't understand why struct Derived1 * and struct Base * are "not suitable types for aliasing each other" ... because the access to the actual memory of the struct Base is always done via an instance of struct Base. Wouldn't taking the address of any member of any struct result in the same fundamental condition?Knap
@JoeHickey: The rule for what may be aliased (C 2018 6.5 7) does not say anything about how types are represented or accessed. It simply says that an object shall only be accessed via a type compatible with it, a qualified version of such type, certain type substitutions for integers (not applicable to pointers), certain types for aggregates (not applicable to pointers), or a character type. Nothing in that rule says it is okay if the types are related as you say. So reasoning about that relationship is irrelevant.Fame
@JoeHickey: One rule that may apply is C 2018 6.2.6.1 28 says all pointers to structure types shall have the same representation and alignment, and a footnote says this is intended to imply interchangeability as arguments to functions, return values from functions, and members of unions. That could make the union method valid, although there are considerations about what it means to interchange members rather than to reinterpret one as another, and this “same representation” implying “interchangeability” is a kludge in the standard that should be avoided when possible.Fame
@JoeHickey: Regarding your “less than ideal” reservation due to “anything goes”, the reinterpret-through-a-pointer method has the same issue.Fame
Aside: For pointers to qualified types, it would also be a good idea to use a pointer to qualified void in the cast, otherwise you would get warnings for the -Wcast-qual option.Fermanagh
@EricPostpischil - good point that the pointer-to-pointer method is just as weak type-wise as the void* method. Your update gets more at the crux of the issue, I think (and my understanding of the aliasing rule). I'm often trying to eek as much type-safety as possible into code that is constrained to embedded C environments (not C++) and so I do try to avoid void* if possible. Perhaps I just need to accept that there is no good/safe(-ish) way of doing this!Knap
@JoeHickey: For type safety, define a static inline function that takes one pointer type and returns another, like static inline struct Derived1 *BaseToDerived1(struct Base *bp). Put whatever implementation you decide to go with inside it. Then you only have to check one function carefully and you can use it throughout the code. (Maybe make an additional version if you need a const-qualified version.)Fame
T
3

Linux kernel offers an interesting alternative to described concepts. It's based on idea of embedding.

struct Base {
  int x;
};

struct Derived {
  struct Base base; // base is embedded into Derived
  int y;
};

Transformation from a pointer to derived is easy:

struct Derived derived;
struct Base* base = &derived.base;

The reverse transformation is done using container_of macro. This macro is bit tricky but it can be simplified subtracting offset of base member within Derived from the pointer to Base.

(struct Derived *)(void*)((char*)base - offsetof(struct Derived, base))

Casting to char* is essential because:

  • allow pointer arithmetic with result of offsetof()
  • as char* can alias with everything, the code is correct with strict aliasing rules
  • casting through void* is used to silence -Wcast-align=strict as resulting object will be correctly aligned

Examplary usage:

struct Derived* derived = container_of(base, struct Derived, base);

This approach is attractive because:

  • the pointer operation is a simple subtraction, no need do dereference any pointer, no extra cache miss
  • the base class can be located anywhere within the derived structure
  • easy support for multiple inheritance
  • structure layout can be modified without breaking existing code

With a help of function pointers, the polymorphism can be robustly implemented.

#include <stdio.h>
#include <stddef.h>

#define container_of(ptr, type, member) \
  (type*)(void*)((char*)ptr - offsetof(type, member))

struct Person {
  int age;
  void (*greet)(struct Person *);
};

struct NamedPerson {
  char name[32];
  struct Person base;
};

void NamedPerson_greet(struct Person *p) {
   struct NamedPerson *np = container_of(p, struct NamedPerson, base); 
   printf("Hello!. My name is %s, and I'm %d years old.\n",
          np->name, np->base.age);
}

struct NamedPerson George = {
  .name = "George",
  .base.age = 42,
  .base.greet = NamedPerson_greet,
};

int main() {
  struct Person *person = &George.base;
  person->greet(person); // Hello, my name is George ...
}

Compiles without any warnings with all aliasing warnings enabled, and in pedantic mode.

gcc prog.c -std=c99 -Wall -pedantic -fstrict-aliasing -Wstrict-aliasing -Wcast-align=strict -O3
Tugboat answered 1/3, 2021 at 21:2 Comment(9)
This is valid but so long as we know/ensure the "base" is always the first element (and don't have any cases of multiple inheritance) then this is basically just a macro-ized version of the previous response. Though I agree it is easier to read by not cluttering the code with casts.Knap
This pattern won't work in cases where the size of the common portion of structures isn't a multiple of the worst-case alignment therein. What's maddening is that the Standard doesn't provide a way of indicating that code which converts a T1* to a T2* might use the resulting pointer to inspect members of a T1, at least until the next time the storage is addressed using type T1 or code enters a context wherein that occurs, probably because the ability to recognize such things is a quality-of-implementation issue outside the Standard's jurisdiction. Too bad the response to...Flotation
...questions about whether compilers had to allow for such possibilities wasn't "Such allowance isn't required for minimal conformance, but is left as a quality-of-implementation matter. A deficient implementation could be conforming without allowing for any cases beyond those expressly listed, but useful implementations should of course do better than that."Flotation
@Flotation Note that the pointer struct Person is transformed to struct NamedPerson if and only if the former pointer originated from NamePerson::base. Therefore all pointers will be correctly aligned because there were aligned when struct NamedPerson object was created.Tugboat
@tstanisl: If the common portion of the structures contains e.g. a double and a short, then a "derived" structure whose unique content starts with a char or short could place that at offset 10 if it contained the common elements directly within itself, rather than a nested structure, and client code exploited C89's Common Initial Sequence guarantee. Placing the shared parts in their own structure would, on most 32-bit or 64-bit platforms, prevent the unique content from starting before address 12 or 16.Flotation
@supercat, ok, so it is a matter of correctness but rather efficiency of memory usage. Using embedded struct will likely result in extra padding bytes. Right?Tugboat
@tstanisl: If one needs to exchange structures with external code that expects structure layouts that don't include omits the padding that wasn't necessary in C89, it's a matter of correctness. Further, the only way the "strict aliasing rules" make any sense is if they are interpreted as allowing access via either an lvalue of an appropriate type, or something which is freshly visibly derived from something of an appropriate type. The question of exactly what constructs compilers recognize is a quality of implementation issue, but was never intended to invite compilers to be...Flotation
...willfully blind. Further, the Standard provides that within parts of the code where a complete union declaration containing two structure types is visible, the structure types may be used interchangeably to inspect common initial; sequence members. The authors of clang and gcc claim that the phrase "anywhere that a declaration of the completed type of the union is visible" only refers to situations that directly access union type lvlaues, and such an interpretation might be tenable if they made any effort to notice when a pointer to one structure type is derived from one of another, but...Flotation
...to be willfully blind to both the Standard-provided means by which programmers can indicate that two structure types might alias, and to any other evidence that a pointer to one structure was derived from another that shares a common initial sequence is at best a mark of a poor-quality implementation.Flotation
A
1

To be clear, the original code is correct and does not require re-working ; the only issue is an unaesthetic warning .

The rest of the question, and the answers so far, focus on how to mangle the code in order to coax the compiler to not issue a warning .

IMHO it's preferable to deal directly with undesirable warnings, instead of mangling the code. Because mangled code is harder to read and understand; and future versions of the compiler might change the way that warnings are triggered.

Approaches along this line would include:

  • disabling that warning entirely
  • conditionally disabling that warning for sections of code that perform the safe operation (perhaps with assistance of a macro or inline function)
  • filtering the compiler output (e.g. via grep -v) to remove the warning
Abhorrence answered 1/3, 2021 at 21:11 Comment(1)
I agree about the general philosophy of (not) changing code to deal with compiler nuances, the flip side is that turning off a warning in its entirety can hide valid warnings, not just the nuisance ones. So if there is a way to "rephrase" the code such that it doesn't trigger the warning, its often worthwhile. (especially when working within organizations that have requirements about what compiler flags/warnings to enable and don't let you change them easily).Knap

© 2022 - 2024 — McMap. All rights reserved.