boltup_im_coding
boltup_im_coding

Reputation: 6665

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));
         }
    }
}

Upvotes: 2

Views: 465

Answers (4)

gnasher729
gnasher729

Reputation: 52566

In C and Objective-C, this is fine in practice. (In theory, the compiler must see the declaration of a union containing both structs as members).

In C++ (and Objective-C++), the language very carefully describes when this is safe and when it isn't. For example, if you start with

typedef struct {
    public: 
    ...

then the compiler is free to re-arrange where struct members are. If the struct uses no C++ features then you are safe.

Upvotes: 0

Theodoros Chatzigiannakis
Theodoros Chatzigiannakis

Reputation: 29223

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

Upvotes: 4

AndersK
AndersK

Reputation: 36092

there is no perfect solution but one way is to use a union

typedef union car_union
{
  CarV1 v1;
  CarV2 v2;

} Car;

that way the size will not differ when you do a memcpy - if version v1 then v2 specific parts will not be initialized.

Upvotes: 0

Mike Sokolov
Mike Sokolov

Reputation: 7054

What you wrote will almost work, except that instead of memcpy(&car, ... you should just have memcpy (car, ..., but there is no reason to use memcpy in such a case. Rather, you should just copy each of the fields in a separate statement.

car->property1 = my_car.property1

(is my_car a pointer or not? it's impossible to tell from the code fragment)

For the second case, I think you can just assign the entire struct: *car = my_car

Upvotes: 0

Related Questions