I am analyzing some code that includes (after simplification) the following construct:
struct some_array {
void *elements;
int nof_elements;
bool has_pointers;
};
The elements
member can either be an array of my_type
elements, or an array of pointers to my_type
elements. Which of the two it is, depends on the value of has_pointers
.
The elements
array contents are allocated on the heap, for example like this:
struct some_array arr = {.nof_elements = 10};
arr.has_pointers = false;
arr.elements = malloc(arr.nof_elements * sizeof(my_type));
or
struct some_array arr = {.nof_elements = 10};
arr.has_pointers = true;
arr.elements = malloc(arr.nof_elements * sizeof(my_type *));
for (size_t i=0; i<arr.nof_elements; i++) {
((my_type**)arr.elements)[i] = malloc(sizeof(my_type));
}
The access logic for the i
th element is like this:
my_type *elmt;
if (arr->has_pointers) {
elmt = ((my_type **)arr->elements)[i];
} else {
elmt = &((my_type *)arr->elements)[i];
}
Assuming that the has_pointers
member is properly set according to the actual contents of elements
, this seems work fine. But it is not clear to me whether this construct complies with the strict aliasing rule? Does it comply with this prescription in the C language specification?
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,
I have trouble applying the definition of effective type in this case and I can not find a definition of the meaning of compatible in this statement.
What you're doing is not a strict aliasing violation, and is in fact well defined.
In the first case:
struct some_array arr = {.nof_elements = 10};
arr.has_pointers = false;
arr.elements = malloc(arr.nof_elements * sizeof(my_type));
arr.elements
points to space for an array of an array of my_type
. Memory obtained via malloc
is suitably aligned for any purpose, so there's no alignment issue. Converting the value of arr.elements
to type my_type *
correctly allows you to access an array of that time.
Similarly for the second case:
struct some_array arr = {.nof_elements = 10};
arr.has_pointers = true;
arr.elements = malloc(arr.nof_elements * sizeof(my_type *));
for (size_t i=0; i<arr.nof_elements; i++) {
((my_type**)arr.elements)[i] = malloc(sizeof(my_type));
}
arr.elements
points to space for an array of an array of my_type *
. You then cast it to that type my_type *
to correctly allow access to an array of that type and assign the values.
Then when you later do this:
my_type *elmt;
if (arr->has_pointers) {
elmt = ((my_type **)arr->elements)[i];
} else {
elmt = &((my_type *)arr->elements)[i];
}
The first part is fine because arr->elements
was treated as an array of my_type *
and is again used that way, and an element of that array is assigned to elmt
. Similarly, the second part is fine because arr->elements
was treated as an array of my_type
, and the address of an element of that array is assigned to elmt
.