Extending a struct in C

前端 未结 11 895
你的背包
你的背包 2020-12-23 15:54

I recently came across a colleague\'s code that looked like this:

typedef struct A {
  int x;
}A;

typedef struct B {
  A a;
  int d;
}B;

void fn(){
  B *b;         


        
相关标签:
11条回答
  • 2020-12-23 16:44

    Yes, it will work. And it is one of the core principle of Object Oriented using C. See this answer 'Object-orientation in C' for more examples about extending (i.e inheritance).

    0 讨论(0)
  • 2020-12-23 16:48

    Yes, it will work cross-platform(a), but that doesn't necessarily make it a good idea.

    As per the ISO C standard (all citations below are from C11), 6.7.2.1 Structure and union specifiers /15, there is not allowed to be padding before the first element of a structure

    In addition, 6.2.7 Compatible type and composite type states that:

    Two types have compatible type if their types are the same

    and it is undisputed that the A and A-within-B types are identical.

    This means that the memory accesses to the A fields will be the same in both A and B types, as would the more sensible b->a.x which is probably what you should be using if you have any concerns about maintainability in future.

    And, though you would normally have to worry about strict type aliasing, I don't believe that applies here. It is illegal to alias pointers but the standard has specific exceptions.

    6.5 Expressions /7 states some of those exceptions, with the footnote:

    The intent of this list is to specify those circumstances in which an object may or may not be aliased.

    The exceptions listed are:

    • a type compatible with the effective type of the object;
    • some other exceptions which need not concern us here; and
    • an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union).

    That, combined with the struct padding rules mentioned above, including the phrase:

    A pointer to a structure object, suitably converted, points to its initial member

    seems to indicate this example is specifically allowed for. The core point we have to remember here is that the type of the expression ((A*)b) is A*, not B*. That makes the variables compatible for the purposes of unrestricted aliasing.

    That's my reading of the relevant portions of the standard, I've been wrong before (b), but I doubt it in this case.

    So, if you have a genuine need for this, it will work okay but I'd be documenting any constraints in the code very close to the structures so as to not get bitten in future.


    (a) In the general sense. Of course, the code snippet:

    B *b;
    ((A*)b)->x = 10;
    

    will be undefined behaviour because b is not initialised to something sensible. But I'm going to assume this is just example code meant to illustrate your question. If anyone's concerned about it, think of it instead as:

    B b, *pb = &b;
    ((A*)pb)->x = 10;
    

    (b) As my wife will tell you, frequently and with little prompting :-)

    0 讨论(0)
  • 2020-12-23 16:49

    I think the OP and many commenters have latched onto the idea that the code is extending a struct.

    It is not.

    This is and example of composition. Very useful. (Getting rid of the typedefs, here is a more descriptive example ):

    struct person {
      char name[MAX_STRING + 1];
      char address[MAX_STRING + 1];
    }
    
    struct item {
      int x;
    };
    
    struct accessory {
      int y;
    };
    
    /* fixed size memory buffer.
       The Linux kernel is full of embedded structs like this
    */
    struct order {
      struct person customer;
      struct item items[MAX_ITEMS];
      struct accessory accessories[MAX_ACCESSORIES];
    };
    
    void fn(struct order *the_order){
      memcpy(the_order->customer.name, DEFAULT_NAME, sizeof(DEFAULT_NAME));
    }
    

    You have a fixed size buffer that is nicely compartmentalized. It sure beats a giant single tier struct.

    struct double_order {
      struct order order;
      struct item extra_items[MAX_ITEMS];
      struct accessory extra_accessories[MAX_ACCESSORIES];
    
    };
    

    So now you have a second struct that can be treated (a la inheritance) exactly like the first with an explicit cast.

    struct double_order d;
    fn((order *)&d);
    

    This preserves compatibility with code that was written to work with the smaller struct. Both the Linux kernel (http://lxr.free-electrons.com/source/include/linux/spi/spi.h (look at struct spi_device)) and bsd sockets library (http://beej.us/guide/bgnet/output/html/multipage/sockaddr_inman.html) use this approach. In the kernel and sockets cases you have a struct that is run through both generic and differentiated sections of code. Not all that different than the use case for inheritance.

    I would NOT suggest writing structs like that just for readability.

    0 讨论(0)
  • 2020-12-23 16:50

    Perhaps you can consider using macros to implement this feature, the need to reuse the function or field into the macro.

    0 讨论(0)
  • 2020-12-23 16:53

    I think Postgres does this in some of their code as well. Not that it makes it a good idea, but it does say something about how widely accepted it seems to be.

    0 讨论(0)
提交回复
热议问题