Is it allowed to create objects at non-sizeof offsets from each other in an allocated memory region?

Viewed 47

This is best explained by example.

typedef struct s_ {
  int a, b;
} s;

int add(s* l, s* r) { return l->a + l->b + r->a + r->b; }

void init(s* v) {
    v->a = 1;
    v->b = 2;
}

int array_like() {
  // allocate enough space for s[2]
  char *p = malloc(2 * sizeof(s));
  s *p1 = (s *)p;
  s *p2 = (s *)(p + sizeof(s));

  init(p1);
  init(p2);
  return add(p1, p2);
}

int array_skip() {
  // allocate enough space for s[3] and init only [0] and [2]
  char *p = malloc(3 * sizeof(s));
  s *p1 = (s *)p;
  s *p2 = (s *)(p + 2 * sizeof(s));

  init(p1);
  init(p2);
  return add(p1, p2);
}

int half_gap() {
  // allocate enough space for 2.5 s objects and lay them
  // out like [s1][gap of alignof(s) bytes][s2]
  char *p = malloc(2 * sizeof(s) + _Alignof(s));
  s *p1 = (s *)p;
  s *p2 = (s *)(p + sizeof(s) + _Alignof(s));

  init(p1);
  init(p2);
  return add(p1, p2);
}

For concreteness, consider a typical platform where sizeof(s) == 8 and alignof(s) == 4 – although the question should apply equally to platforms with different values.

The last three functions, array_like, array_skip and half_gap all perform a similar function: they create two objects of type s (a struct containing two ints) inside storage allocated by malloc. All three place the first s object at the start of the storage. They differ only in where they place the second object:

  • array_like places it directly after the first object, i.e., at offset 8, so for a pointer s* p to the start of the region the objects would be at p[0] and p[1].
  • array_skip places it sizeof(s) bytes after the end of the first object, i.e., at offset 16, so for a pointer s* p to the start of the region the objects would be at p[0] and p[2].
  • half_gap places it _Alignof(s) bytes after the end of the first object, or 4 bytes after the first object, i.e., 12 bytes from the start of the storage. Note that this object is still correctly aligned on a boundary of 4 bytes, but it is not offset an integral number of sizeof(s) bytes away from the first. You can't express the location of the second object with array-like notation as in the first two cases.

After that, each function writes to both members of both objects, and then reads from them.

Which of these functions are legal C11, and are all legal functions guaranteed to return the expected value of 6?

1 Answers

The code in your question is correct. The pointer cast is defined if and only if it is correctly aligned for the type being cast to.

(Footnote - I have seen people argue that 6.3.2.3/7 does not clearly specify that the result of the conversion points to the same byte in memory -- however if that argument is accepted then malloc is unusable since it would not guarantee that converting the void * to whatever type for future usage points into the allocated block. So I don't consider that argument valid).

Because of the requirement for arrays to have no padding between elements, we can conclude that sizeof(T) must be a multiple of _Alignof(T), and therefore all of your integer expressions are multiples of _Alignof(T) and therefore all of the pointers in question are correctly aligned.

Regarding terminology, object means "a region of storage" in C. So the whole space allocated by malloc is an object, as is any contiguous subset of it.

Using the assignment operator modifies an object, not creating it. When you use the assignment operator on malloc'd space, it sets the effective type of the bytes which are written . C11 6.5/6 (aka. the strict aliasing rule) defines the meaning of "effective type".


There's one other detail. If your init function looked like this:

void init(s* v) {
    s a = { .a = 1, .b = 2 };
    *v = a;
}

then this would be the end of the story, a value of type s is written to the location. But the standard isn't clear about which effective type is set by v->a = 1;. The most common interpretation of the standard is that v->a = 1 means (*v).a = 1; and this assignment also has the "side effect" of setting effective type for an entire *v object.

This interpretation enables TBAA on a function like void f(s *ps, t* pt) (where s is a struct with no members of type t) to assume that *ps and *pt are disjoint.

I'm sure this latter point already has multiple questions on SO about it

Related