Search code examples
cstrict-aliasing

Strict aliasing rule when storing and accessing an array of (pointers to) elements


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 ith 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.


Solution

  • 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.