Search code examples
cstructgeneric-programming

Multiple structs, same fields that need to be accessed in a method


I currently try to write some lil literal console game for fun in C.

For that, i need to be able to print window-like structures in ... well ... C.

I want to use a generic rendering method (lets call it frame_render(...)) to render all different "ui elements"

The problem now is: how to solve this?

given the scenario:

// Theoretical base
struct frame { int x; int y; int width; int height; }
struct button { int x; int y; int width; int height; ... }
struct whatever { int x; int y; int width; int height; ... }

how could i ensure that my x, y, width and height are always in the correct spot memory wise? is it enough to "just" put them in the same order at the very begining?

also, how to design the method header to accept it?


Solution

  • is it enough to "just" put them in the same order at the very begining?

    Yes, if you're careful as you've done above.

    also, how to design the method header to accept it?

    There are different ways to do this.

    Here's an example, using the [ugly] equivalent of a c++ "base" class:

    enum type {
        FRAME,
        BUTTON,
        WHATEVER
    };
    
    struct geo {
        int x;
        int y;
        int width;
        int height;
        enum type type;
    };
    
    struct frame {
        struct geo geo;
    };
    
    struct button {
        struct geo geo;
        int updown;
    };
    
    struct whatever {
        struct geo geo;
        int what;
        int ever;
    };
    
    void
    frame_render(struct geo *geo)
    {
        struct frame *frm;
        struct button *but;
        struct whatever *what;
    
        switch (geo->type) {
        case FRAME:
            frm = (struct frame *) geo;
            frame_render_frame(frm);
            break;
    
        case BUTTON:
            but = (struct button *) geo;
            printf("x=%d y=%d updown=%d\n",geo->x,geo->y,but->updown);
            frame_render_button(but);
            break;
    
        case WHATEVER:
            what = (struct whatever *) geo;
            printf("x=%d y=%d what=%d ever=%d\n",
                what->geo.x,what->geo.y,what->what,what->ever);
            frame_render_whatever(what);
            break;
        }
    }
    

    Here's a way to use a virtual function table:

    enum type {
        FRAME,
        BUTTON,
        WHATEVER
    };
    
    struct geo;
    
    // virtual function table
    struct virtfnc {
        void (*calc)(struct geo *);
        void (*render)(struct geo *);
    };
    
    struct geo {
        int x;
        int y;
        int width;
        int height;
        enum type type;
        struct virtfnc *fnc;
    };
    
    struct frame {
        struct geo geo;
    };
    
    struct button {
        struct geo geo;
        int updown;
    };
    
    struct whatever {
        struct geo geo;
        int what;
        int ever;
    };
    
    void
    frame_render(struct geo *geo)
    {
        struct frame *frm = (struct frame *) geo;
    
        // whatever
    }
    
    void
    button_render(struct geo *geo)
    {
        struct button *but = (struct button *) geo;
    
        // whatever
    }
    
    void
    whatever_render(struct geo *geo)
    {
        struct whatever *what = (struct whatever *) geo;
    
        // whatever
    }
    
    void
    any_render(struct geo *geo)
    {
    
        geo->fnc->render(geo);
    }
    

    Here's a third way that uses a union. It is simpler but requires that the base struct be as large as the largest sub-class:

    enum type {
        FRAME,
        BUTTON,
        WHATEVER
    };
    
    struct frame {
        ...
    };
    
    struct button {
        int updown;
    };
    
    struct whatever {
        int what;
        int ever;
    };
    
    struct geo {
        int x;
        int y;
        int width;
        int height;
        enum type type;
        union {
            struct frame frame;
            struct button button;
            struct whatever what;
        } data;
    };
    
    void
    any_render(struct geo *geo)
    {
    
        switch (geo->type) {
        case FRAME:
            render_frame(&geo->data.frame);
            break;
    
        case BUTTON:
            render_button(&geo->data.button);
            break;
    
        case WHATEVER:
            render_whatever(&geo->data.what);
            break;
        }
    }
    

    UPDATE:

    is this approach casting safe? eg. putting all into some array that is of the type frame* and then just accessing frame->geo? or would that cause any problems with later calls to free(..)?

    No problem with free if allocations are done with the derived types (e.g. frame, button), but not the base type geo: malloc(sizeof(struct button)).

    To have a simple array [of shapes], the union method would need to be used (i.e. all derived structs must have the same size). But, this would be wasteful if we had some subtype that used a lot more space than the others:

    struct polyline {
        int num_points;
        int x[100];
        int y[100];
    };
    

    This could still be done with methods #1 or #2 [where the subtype structs are of different sizes] with an indirect pointer array:

    void
    all_render(struct geo **glist,int ngeo)
    {
    
        for (;  ngeo > 0;  --ngeo, ++glist)
            any_render(*glist);
    }
    

    Rather than an array of different shapes, I'd consider a [doubly] linked list. This allows the subtype structs to have different sizes. We'd add a struct geo *next element to struct geo. Then, we could do:

    void
    all_render(struct geo *geo)
    {
    
        for (;  geo != NULL;  geo = geo->next)
            any_render(geo);
    }
    

    The list approach may be preferable, particularly if we add/remove shapes on a dynamic basis [or reorder them based on Z-depth].

    Or, some shapes might contain others. So, we could add struct geo *children to struct geo. Then, it's easy to (e.g.) draw a containing box, then all the shapes within that box by traversing the children list. If we go for children, we may as well add struct parent *parent as well so each shape knows what shape contains it.