6

Take the following example:

typedef struct array_struct {
    unsigned char* pointer;
    size_t length;
} array;

typedef struct vector_struct {
    unsigned char* pointer;
    // Reserved is the amount of allocated memory not being used.
    // MemoryLength = length + reserved;
    size_t length, reserved;
} vector;


// Example Usage:
vector* vct = (vector*) calloc(sizeof(vector), 1);
vct->reserved = 0;
vct->length = 24;
vct->pointer = (unsigned char*) calloc(arr->length, 1);

array* arr = (array*) vct;
printf("%i", arr->length);
free(arr->pointer);
free(arr);

C seems to allocate memory for struct members in the order they're defined in the struct. Which means that if you cast vector -> array you'll still get the same results if you perform operations on array as you would as if you did it on vector since they have the same members and order of members.

As long as you only down cast from vector -> array as if array was a generic type for vector you shouldn't run into any problems.

Is this undefined and bad behavior despite the similar structure of the types?

Barmar
  • 741,623
  • 53
  • 500
  • 612
FatalSleep
  • 307
  • 1
  • 2
  • 15
  • 4
    You're assuming `array` and `vector` have the same padding, which I don't think is guaranteed. – Cornstalks Jun 04 '16 at 15:23
  • Why wouldn't they? The padding shouldn't differ if they have the same structure. Any additional padding would be at the end, outside of the structure, where it shouldn't matter. – FatalSleep Jun 04 '16 at 15:26
  • I can see why this might be irregular behavior, but not 100% sure if it's undefined and posses any actual threat. – FatalSleep Jun 04 '16 at 15:27
  • 2
    There are some related questions on SO, but no consistent answers to them: [Some](https://stackoverflow.com/questions/3995940/casting-one-c-structure-into-another#comment4278883_3995969) call it undefined behavior, [others](http://stackoverflow.com/a/3766251/1829930) call it well-defined C code. – Robin Krahl Jun 04 '16 at 15:30
  • 1
    @RobinKrahl The 3rd answer in the second question says it's not. It cites an article on the strict-aliasing rule. – Barmar Jun 04 '16 at 15:32
  • @JohnColeman is there any way I could check how the compiler is padding the structs? – FatalSleep Jun 04 '16 at 15:33
  • 1
    @FatalSleep I would imagine that the answer is compiler-specific. `gcc` is open source so in principle, yes you can discover how it pads. – John Coleman Jun 04 '16 at 15:35
  • 2
    I would also expect that in most compilers it will do what you expect. It's hard to imagine a CPU architecture that would need different alignment/padding of earlier struct members depending on later members. – Barmar Jun 04 '16 at 15:38

1 Answers1

6

This is well-defined behavior if you permit type aliasing (which C doesn't but most compilers do, either by default or by some compilation flag), and it is undefined behavior if you prohibit this type of type aliasing (which is commonly referred to as "strict aliasing" because the rules are pretty strict). From the N1570 draft of the C standard:

6.5.2.3

6 One special guarantee is made in order to simplify the use of unions: if a union contains several structures that share a common initial sequence (see below), and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the complete type of the union is visible. Two structures share a common initial sequence if corresponding members have compatible types (and, for bit-fields, the same widths) for a sequence of one or more initial members.

That section is about unions, but in order for that behavior to be legal in unions, it restricts padding possibilities and thus requires the two structures to share a common layout and initial padding. So we've got that going for us.

Now, for strict aliasing, the standard says:

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 "compatible type" is:

6.2.7

1 Two types have compatible type if their types are the same.

It goes on to explain that more and list a few cases that have a little more "wiggle room" but none of them apply here. Unfortunately for you, the buck stops here. This is undefined behavior.

Now, one thing you could do to get around this would be:

typedef struct array_struct {
    unsigned char* pointer;
    size_t length;
} array;

typedef struct vector_struct {
    array array;
    size_t reserved;
} vector;
Cornstalks
  • 37,137
  • 18
  • 79
  • 144
  • 1
    I just wanted to post the same quote, though I disagree that we can deduce that what the OP does is well defined. But I agree that it should work in practice. – alain Jun 04 '16 at 15:43
  • The meat is here "*of one **or more***". – alk Jun 04 '16 at 15:46
  • I can't say it's well defined--but when compiled in VS it works like a charm as I assumed. I'll need to check on the GCC. – FatalSleep Jun 04 '16 at 15:47
  • 2
    "I should add that it's perhaps possible that strict aliasing rules prohibit this, and this may only be done via a union, but I'm unsure of that." That's correct. Just because the structs have the same layout in memory does not mean the compiler considers the types interchangeable. They are still distinct types, and accesses to one type through a pointer to a different type are still undefined. What trips many people up is that the rules of the type system operate on a higher level than what exists in memory or the alignment of that memory. – tab Jun 04 '16 at 15:48
  • @tab that makes sense. Since it's undefined, what harm is caused by casting between these types, if any? – FatalSleep Jun 04 '16 at 15:52
  • "*of one or more*" does not seem to hold if talking about structures or unions in general: 6.7.2.1/15: "*A pointer to a structure object, suitably converted, points to its initial member ...*" not a word on the members following the 1st. – alk Jun 04 '16 at 16:00
  • Yeah, I've had to revise my answer to say that it's undefined behavior due to strict aliasing. The standard convinced me that it's not permitted unless you disable strict aliasing (which most compilers let you do). – Cornstalks Jun 04 '16 at 16:03
  • 1
    @FatalSleep: strict aliasing rules were introduced primarily so the compiler could do really aggressive optimizations. Without strict aliasing, the compiler has to do lots more loads from memory because it's possible two objects alias the same memory, and a modification via one object will affect the next read on another object. But strict aliasing lets the compiler assume two objects of different type refer to different memory locations, so fewer loads have to be done if one object is modified. You can still `memcpy` between the two, but violating strict aliasing can lead the compiler... – Cornstalks Jun 04 '16 at 16:11
  • 1
    ...to make "invalid" optimizations that completely obliterate your program and its intended behavior. – Cornstalks Jun 04 '16 at 16:12