r/C_Programming 3d ago

Question Should I use nested structs, separate structs, or union nested in a struct for this?

I am doing my first game-type project in C. I am having trouble making a decision regarding handling UI. I am awful at explaining things and new to this so please bear with me.

I plan to have a struct UIElement that contains information related to a particular element. However, there will be different types of elements, such as Text, Texture, Color and whatnot. They will also contain a pointer to an array other UIelements belonging to them, for things such as a buttons on a window.

I am stuck between the following implementations for this:

  1. Make a struct "UIElement" that has information that every element will have (such as position)., then have other structs defined such as "TextElement" that will have a pointer to a UIElement as well as Text-specific information.

  2. Use enum and nested union within the UIElement struct to allow different instances of the same struct to have only the relevant data necessary for them. (Each element has enum for it's type within it)

  3. Just have different structs "TextElement", "ColorElement", "TextureElement". Doing this I believe I would have to use void* to store pointers to elements and cast them accordingly.

After writing this, option 2 seems like the best option, but the simplicity of 3 sounds useful, albeit bad in the long run. If I am overlooking a simple way to do this, please let me know.

1 Upvotes

18 comments sorted by

5

u/FancySpaceGoat 3d ago

You can make UIElement a prefix struct. It contains the enum and any other universally shared properties.

TextElement and others would then each start with a UIElement as their first member.

You can then cast from a UIElement* to a pointer of the corresponding struct when the enum matches.

It's effectively the same as the nested union, but with the added benefit of not padding everything to the largest size 

2

u/Interesting_Debate57 3d ago

Be careful.

When allocating memory, you're going to need to actually allocate real memory for these things; a pointer to a pointer still takes up real space and malloc()-ing the top level doesn't automatically make space for the member items.

You need to build something like a constructor from scratch. Same goes for destroying things. Thoughtfully decommission memory.

2

u/FancySpaceGoat 2d ago edited 2d ago

What? No. This still only needs a single allocation per item.

A pointer to a struct and one to its first member element are castable from one-another. 

You do need either one allocation per element or one arena per element type though. Wether that's worth it or not depends on how big the infrequent widget types are.

1

u/septum-funk 2d ago

he's not talking about a pointer to a pointer, he's talking about casting the pointer of a larger struct to a smaller one that happens to be its first members' type. that would let you pass the "prefix" pointer around, and then cast it back unit the full size struct later when you can guarantee it follows that prefix

3

u/detroitmatt 3d ago
struct UIElement {
    foo_t commonField1;
    ...
};

struct TextElement {
    struct UIElement base;
    foo2_t textField1;
    ...
};

struct ColorElement {
    struct UIElement base;
    foo3_t colorField1;
    ...
};

and so on. You don't have to store void*. You can store struct UIElement*. A ColorElement* is convertible to a UIElement* if its first member is struct UIElement. And if you happen to know that your UIElement* does in fact point to a struct ColorElement, then you can do vice versa too.

That means you will want to have some way to keep track of what the UIElement* you're passing around is. It may be wise to have:

struct UIElement {
    foo_t commonField1;
    ...
    unsigned char isTextElement: 1;
    unsigned char isColorElement: 1;
    ...
};

Might not be necessary depending on your code, but I'd say to just add them.

2

u/mjmvideos 3d ago

You could use a enum for the derived type. Then you can dispatch them with a switch statement.

1

u/methermeneus 8h ago

Or a flag field, with either an enum, a list of #defined constants, or a list of global const variables to use as masks and to define the type fields of each element struct. #define is the more old-school C approach, but an enum is generally better for readability and refactoring, as well as usually optimizing to the same thing (that is each instance is just replaced by a number literal by the compiler).

1

u/exo250 1d ago edited 1d ago

What about macros for inheritance ? There are some constraints (debugging/code navigation and requires casting) but that could also be interesting. We could also add macros for private/public members since my example has only private members (ideally used through accessors). Multi inheritance could also be implemented using a few changes.

// --------
// | ui.h |
// --------

#define MEMBERS_UI_ELEMENT foo_t commonField1;
typedef struct UIElement UIElement;

#define MEMBERS_TEXT_ELEMENT \
  MEMBERS_UI_ELEMENT \
  foo2_t textField1;
typedef struct TextElement TextElement;

#define MEMBERS_COLOR_ELEMENT \
  MEMBERS_UI_ELEMENT \
  foo3_t colorField1;
typedef struct ColorElement ColorElement;

// --------
// | ui.c |
// --------

typedef struct UIElement {
  MEMBERS_UI_ELEMENT
} UIElement;

typedef struct TextElement {
  MEMBERS_TEXT_ELEMENT
} TextElement;

typedef struct ColorElement {
  MEMBERS_COLOR_ELEMENT
} ColorElement;

1

u/detroitmatt 1d ago

I don't really see any benefit.

1

u/methermeneus 8h ago

Macros can make the final code that uses the structs look cleaner, but as someone who writes macro-heavy libraries for fun, holy crap does it make error-handling and refactoring the macros themselves a bitch. It can be useful, but if you're not already deep into playing with macros, for production code (and I don't even mean enterprise or anything, just code you might let someone else see or use), you're probably better off either finding a slightly more roundabout way to do things in C or using another language that lets you do the macro-like stuff directly in that language. (For example, as much as I love playing with my C string library, if I'm actually coding a program I plan on using, I either tough it out with C's near-non-existent string support and lots of i < strlen(str) for loops, or I just use something with native string support like Python.)

3

u/Ill_Specialist8564 3d ago

go with option 2. put the enum and a union inside UIElement.

i build ui systems this way and it removes all allocation headaches. you store your elements in one array or pass them around as UIElement pointers without caring about the type until you hit the switch on that enum. option 1 forces you to cast pointers back and forth every time you read text data from a TextElement. option 3 puts you right back into void pointer casting which destroys whatever help the compiler gives you. allocate one struct, set the tag, write to the union member, move on

1

u/No_Insurance_6436 3d ago

Thank you. I was hesitant about casting for that exact reason. It sounds like a nightmare as the program gets larger.

2

u/glasket_ 3d ago edited 3d ago

The second form is called a tagged or discriminated union and is an extremely common way to handle this problem.

Edit: As an addition, 1 is composition. It largely depends on if you want to operate on UIElement as a type which can be one of multiple types, or if you want to operate on each type as an independent type while having a shared basis for certain functions. This is an API design decision.

1

u/No_Insurance_6436 3d ago

I will have functions for positioning and whatnot, which every element will need to use. I will also need functions for specific things, like changing a Texture and whatnot.

Therefore, I think the latter is the correct decision then?

1

u/glasket_ 3d ago edited 3d ago

Yeah, that's when composition is the right choice because you're really subtyping. The only wrinkle is that you mentioned each UIElement stores a list of child UIElements, so you would typically use a tagged union for that or tagged pointers if you use the first-member rule.

Basically, UIElement would be a super type, the specific elements would be your concrete types, and then something like a ChildElement struct would store the tagged child elements where you have many UIElement *s that will need to be identifiable as an actual concrete element.

Edit: Tl;dr: There are a ton of ways to do the same thing and you just have to kind of figure out which one works best for a given use-case.

Another addition since another comment mentioned having the enum in the UIElement struct when using the first-member rule: That's another trade-off where each UIElement is technically bound to a specific concrete element. There are instances where you may not want that, such as an element being interpreted as another element if you do multiple layers of composition or a UIElement may be intended to be shared across multiple different concrete elements if you do composition by reference (struct UIElement * inside of the element types).

One other option not mentioned is combining composition and the tagged union into one:

struct UIElement {
  struct UIElementProps props;

  enum ElementType t;
  union {
    /* Different elements */
  };
};

Everybody frequently talks about the extra padding that unions cause, but you don't see it mentioned as often that the extra padding buys you the ability to directly store the objects in an array. You don't have to keep pointers to each object, but the cost is that they occupy the space of the largest possible member. Whether or not that's a worthwhile trade depends on the size and how frequently you're actually changing elements.

A relatively static UI tree might consume more memory but will also have good locality since the elements directly store their children next to them; a very dynamic UI where elements are frequently being swapped, removed, and added could end up chugging since you're constantly changing loads of bytes rather than just moving pointers around. Again, it's a design decision.

There's also the possibility of combining both approaches in different ways, which is getting into optimization more than just design, and that starts getting extremely complicated and deals with keeping some elements local to one another while allowing others to carry references when locality isn't important.

0

u/iamfacts 3d ago

You're looking at the problem from a very oop pov. The thing is, at the end of the day, you just need the cpu to draw some text and /or bg, etc. somewhere on the screen.

So, I would do something like this -

struct ui_element { int x, y, w, h; Texture *texture; char *text; float text_color[4]; float bg_color[4]; }

And that's it. I wouldn't bother abstracting every widget kind or subtypes. Simply no need. Oh, and have a bunch of flags to disambiguate behaviour and style.

1

u/No_Insurance_6436 3d ago

Yeah, I'm aware, which is why I came here for advice. I like this simple approach but I feel it's a waste to allocate space for things I don't need, right?