user539810
user539810

Reputation:

Confused by effective type rules

I seem to be missing a few pieces of the puzzle regarding effective type again... The comments in the code are essentially my question, but it's the only way I can think to ask this question with proper context.

#include <stdlib.h>
#include <string.h>

typedef struct foo {
    int n;
} Foo;

int main(void)
{
    // No effective type yet because there has been no write to the memory.
    Foo *f = malloc(sizeof(Foo));

    // Effective type of `f` is now `Foo *`, except I'm writing to
    // `f->n`, so shouldn't it be `int *`? Not sure what's going on here.
    f->n = 1;

    // No effective type yet because there has been no write to the memory.
    char *buf = malloc(sizeof(Foo));

    // Effective type of `buf` is `Foo *`, despite it being declared as
    // `char *`.
    // It's not safe to modify `buf` as a `char` array since the effective type
    // is not `char`, or am I missing something?
    memcpy(buf, f, sizeof(Foo));

    // The cast here is well defined because effective type of `buf` is
    // `Foo *` anyway, right?
    ((Foo *)buf)->n++;

    // I'm not even sure this is OK. The effective type of `buf` is `Foo *`,
    // right? Why wouldn't it be OK then?
    memcpy(f, buf, sizeof(Foo));

    // Safe if the last `memcpy` was safe.
    f->n++;

    // buf now points to invalid memory.
    free(buf);

    // Pointers with different declared types point to the same object,
    // but they have the same effective type that is not qualified by
    // `restrict`, so this doesn't violate strict aliasing, right?
    // This is allowed because `f` was allocated via `malloc`,
    // meaning it is suitably aligned for any data type, so
    // the effective type rules aren't violated either.
    buf = (void *)f;

    // `f`, and consequently `buf` since it points to the same object as `f`,
    // now point to invalid memory.
    free(f);
}

Am I correct in thinking that all of that is OK, or am I wrong in some cases? I realize this is borderline asking multiple questions, but I'm essentially asking whether my understanding of effective type and strict aliasing is correct. GCC produced no diagnostics for me with -pedantic-errors -Wextra -Wall -O2 -fstrict-aliasing -Wstrict-aliasing

Upvotes: 3

Views: 349

Answers (3)

2501
2501

Reputation: 25753

// No effective type yet because there has been no write to the memory.
Foo *f = malloc(sizeof(Foo));

There are no accesses here and no objects. Effective type is not relevant.

// Effective type of `f` is now `Foo *`, except I'm writing to
// `f->n`, so shouldn't it be `int *`? Not sure what's going on here.
f->n = 1;

Object at the first sizeof(Foo) bytes at address f, has an effective type Foo, and the object at the first sizeof(int) bytes at address f has an effective type int.

// No effective type yet because there has been no write to the memory.
char *buf = malloc(sizeof(Foo));

There are no accesses here and no objects. Effective type is not relevant.

// Effective type of `buf` is `Foo *`, despite it being declared as
// `char *`.
// It's not safe to modify `buf` as a `char` array since the effective type
// is not `char`, or am I missing something?
memcpy(buf, f, sizeof(Foo));

Object at the first sizeof(Foo) bytes at address but, has an effective type Foo, and the object at the first sizeof(int) bytes at address but has an effective type int.

Any object may be accessed with a character type regardless of its effective type. You may access bytes of buf with char.

// The cast here is well defined because effective type of `buf` is
// `Foo *` anyway, right?
((Foo *)buf)->n++;

Yes. The entire expression is valid.

// I'm not even sure this is OK. The effective type of `buf` is `Foo *`,
// right? Why wouldn't it be OK then?
memcpy(f, buf, sizeof(Foo));

This is ok. memcpy changes the type of object at address f to type Foo. Even if f didn't have type Foo before, it does now.

// Safe if the last `memcpy` was safe.
f->n++;

Yes.

// buf now points to invalid memory.
free(buf);

Yes.

// Pointers with different declared types point to the same object,
// but they have the same effective type that is not qualified by
// `restrict`, so this doesn't violate strict aliasing, right?
// This is allowed because `f` was allocated via `malloc`,
// meaning it is suitably aligned for any data type, so
// the effective type rules aren't violated either.
buf = (void *)f;

You mixing concepts. Restrict and values of individual pointers are not relevant for aliasing. Access is. Pointer buf now simply points to address f.

// `f`, and consequently `buf` since it points to the same object as `f`,
// now point to invalid memory.
free(f);

Yes.

Upvotes: 1

M.M
M.M

Reputation: 141618

You seem to be mixed up about what "effective type" applies to: it applies to the malloc'd space, not any pointer. As always in C, a pointer is a separate object with separate properties to any space that the pointer may happen to point to.

f is a (named) variable so its effective type is always the same as its declared type, namely Foo *. Similarly buf's effective type is always char *. The only time that the effective type may change at runtime is for dynamically allocated space.

Your bullet points and the code comments make little sense so I've decided to annotate your code afresh. The text refers to the code above the text in each case:

Foo *f = malloc(sizeof(Foo));

OK. Uninitialized bytes have been allocated and f points to them. The dynamically allocated space has no effective type yet.

f->n = 1;

The effective type of the first sizeof(int) bytes of dynamically allocated space is set to int. (* - but see footnote)

char *buf = malloc(sizeof(Foo));
memcpy(buf, f, sizeof(Foo));

The memcpy function preserves effective type of objects copied, so the effective type of the first sizeof(int) bytes of the space pointed to by buf, is int.

((Foo *)buf)->n++;

Firstly, the cast does not have an alignment problem because malloc space is correctly aligned for any type. Moving onto the access of n, this is OK because ((Foo *)buf)->n is an lvalue of type int, and it designates an object of effective type int. So we can read and write with no problem.

memcpy(f, buf, sizeof(Foo));

memcpy is always OK, as it sets the effective type (your comment suggested that memcpy might not be OK in some cases). This line sets the effective type of the space pointed to by f, to int (since the source space had effective type int).

f->n++;

Fine, same rationale as ((Foo *)buf)->n++ above.

free(buf);
buf = (void *)f;

Redundant cast. The space pointed to by f has effective type int still, since neither of those lines wrote to that space.

free(f);

No problem.


Footnote: Some people take a different interpretation of the expression f->n (or ((Foo *)buf)->n w.r.t. strict aliasing. They say that f->n is defined as (*f).n and therefore the associated effective type is the type of *f, not the type of f->n. I don't agree with that view so I won't expound on it further. There have been proposals for C2X to clarify this situation and other grey areas of strict aliasing. For your particular code, the code is still correct under either interpretation though.

Upvotes: 4

wallyk
wallyk

Reputation: 57774

The code is valid and—for having to deal with polymorphic data objects—was how it was done before C++.

However, there is not much more that can be extrapolated from those operations before one might shoot oneself in the foot. That might occur from having Foo and say a type Foo2 which is a different size and then access elements in one which are not there because the associated malloc() wasn't big enough.

Generally, it is more understandable and likely to be correct if the pointer type is always the same as the malloc(). For fancier morphism, c++ is likely to be less error prone (as long as its warnings are not suppressed).

Upvotes: 0

Related Questions