Search code examples
coopinheritancememcpy

memcpy Inheritance-like structs - is it safe?


I have two structs I'm working with, and they are defined nearly identical. These are defined in header files that I cannot modify.

typedef struct
{
    uint32_t    property1;
    uint32_t    property2;
} CarV1;

typedef struct
{
    uint32_t    property1;
    uint32_t    property2;
    /* V2 specific properties */
    uint32_t    property3;
    uint32_t    property4;
} CarV2;

In my code, I initialize the V2 struct at the top of my file, to cover all my bases:

static const carV2 my_car =  {
    .property1 =   value,
    .property2 =   value,
    /* V2 specific properties */
    .property3 =   value,
    .property4 =   value
};

Later, I want to retrieve the values I have initialized and copy them into the struct to be returned from a function via void pointer. I sometimes want V2 properties of the car, and sometimes V1. How can I memcpy safely without having duplicate definitions/initializations? I'm fairly new to C, and its my understanding that this is ugly and engineers to follow me in looking at this code will not approve. What's a clean way to do this?

int get_properties(void *returned_car){
     int version = get_version();
     switch (version){
         case V1:
         {
             CarV1 *car = returned_car;
             memcpy(car, &my_car, sizeof(CarV1)); // is this safe? What's a better way?
         }
         case V2:
         {
             CarV2 *car = returned_car;
             memcpy(car, &my_car, sizeof(CarV2));
         }
    }
}

Solution

  • Yes, it's definitely possible to do what you're asking.

    You can use a base struct member to implement inheritance, like this:

    typedef struct
    {
        uint32_t    property1;
        uint32_t    property2;
    } CarV1;
    
    typedef struct
    {
        CarV1       base;
        /* V2 specific properties */
        uint32_t    property3;
        uint32_t    property4;
    } CarV2;
    

    In this case, you're eliminating the duplicate definitions. Of course, on a variable of type CarV2*, you can't reference the fields of the base directly - you'll have to do a small redirection, like this:

    cv2p->base.property1 = 0;
    

    To upcast to CarV1*, do this:

    CarV1* cv1p = &(cv2p->base);
    c1vp->property1 = 0;
    

    You've written memcpy(&car, &my_car, sizeof(CarV1)). This looks like a mistake, because it's copying the data of the pointer variable (that is, the address of your struct, instead of the struct itself). Since car is already a pointer (CarV1*) and I'm assuming that so is my_car, you probably wanted to do this instead:

    memcpy(car, my_car, sizeof(CarV1));
    

    If my_car is CarV2* and car is CarV1* (or vice versa), then the above code is guaranteed to work by the C standard, because the first member of a struct is always at a zero offset and, therefore, the memory layout of those two for the first sizeof(CarV1) bytes will be identical.

    The compiler is not allowed to align/pad that part differently (which I assume is what you meant about optimizing), because you've explicitly declared the first part of CarV2 to be a CarV1.


    Since in your case you are stuck with identically defined structs that you can't change, you may find useful that the C standard defines a macro/special form called offsetof.

    To be absolutely sure about your memory layouts, I'd advise that you put a series of checks during the initialization phase of your program that verifies whether the offsetof(struct CarV1, property1) is equal to offsetof(struct CarV2, property1) etc for all common properties:

    void validateAlignment(void)
    {
        if (offsetof(CarV1, property1) != offsetof(CarV2, property1)) exit(-1);
        if (offsetof(CarV1, property2) != offsetof(CarV2, property2)) exit(-1);
        // and so on
    }
    

    This will stop the program for going ahead in case the compiler has done anything creative with the padding.

    It also won't slow down your program's initialization because offsetof is actually calculated at compile time. So, with all the optimizations in place, the void validateAlignment(void) function should be optimized out completely (because a static analysis would show that the exit conditions are always false).