X39
X39

Reputation: 827

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?

Upvotes: 1

Views: 2598

Answers (3)

Craig Estey
Craig Estey

Reputation: 33621

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.

Upvotes: 3

DYZ
DYZ

Reputation: 57075

The "classical" method is to have a struct that contains a union of all possible objects and an enum that identifies which exactly object has been passed:

struct renderable {
    enum {FRAME, BUTTON, WHATVERE, ...} type;
    union {
        struct frame frame;
        struct button button;
        struct whatever whatever;
        ....
    } data;
};

After passing this struct to the renderer, use a switch on the type field to extract the coordinates, etc:

void renderer (renderable *obj, ...) {
    ...
    switch(obj->type) {
        case FRAME: x = obj->data.frame.x; ...; break;
        case BUTTON: x = obj->data.button.x; ...; break;
        ...
    }
    ...
}

It was reportedly this kind of monstrosity that encouraged Stroustrup to invent C++ :)

Edited

Another "classical" solution is to have a separate struct that has the dimensions and the position of any object:

struct geometry {
    int x, y, width, height;
}

You can store this structure at the beginning of any object-specific struct and use a cast to gain access to it:

struct frame {
    struct geometry geo;
    // more stuff
};

struct frame frame = {....};
rendered((void*)&frame, ...);

In the renderer:

void renderer (void *obj, ...) {
    ...
    struct geometry *geo = (struct geometry *)obj;
    geo->x ...
}

The latter approach may be somewhat unsafe. To make it 100% safe, separate the geometry from the object-specific information and pass them to the renderer as two separate structs.

Upvotes: 2

supercat
supercat

Reputation: 81217

If all of the structures start with members of the same types, in the same order, corresponding members will have the same offsets. Most compilers can be configured to allow a pointer of any structure type to be used to inspect members of the Common Initial Sequence of any other, but there are a few issues:

  1. On some unusual platforms, if an object is followed by padding bytes, an instruction that writes the object and padding bytes together (likely storing meaningless values in the latter) may be faster than instructions that write only the object. If a member is followed by a padding byte in some structures, but by meaningful data in another, a write to that member using the type where it's followed by padding bytes may write overwrite the "padding bytes" with meaningless values, thus corrupting the values of the following members in the other structure types. I am unaware of any architectures in current use where this would be an issue for any struct members other than bitfields, and I am unaware of any current implementations where this would be an issue even for those, but such possibilities could arise on some platforms, especially with bitfields.

  2. Given something like:

    int readField1OfS1(struct s1 *p) { return p->field1; }
    
    struct s2 *myStruct2Ptr;
    
    if (readField1ofS1((struct s1*)myStruct2Ptr)
       ...
    

    some compilers like gcc and clang won't reliably allow for the possibility that the value returned from the function might depend upon a value held by part of the Common Initial Sequence of an object of type struct s2 at the time of the call unless optimizations are disabled (e.g. using the -fno-strict-aliasing option). I would think that presence of a cast from struct s2* to struct s1* within the function call expression should allow a quality compiler to recognize that anything function might do something with an object of type struct s1 might be done on a struct s2, but since the Standard doesn't explicitly require that, the authors of gcc and clang refuse to make any effort to reliably recognize such constructs even in straightforward cases like the above.

Code which makes use of the Common Initial Sequence rule will work reliably on nearly any suitably-configured compiler, but some like gcc and clang must be specially configured using -fno-strict-aliasing option. The ability to exploit Common Initial Sequence guarantees has been a well-established part of the language since 1974, and when the Standard was written, anyone familiar with the language would have understood that it was designed to allow for constructs like those above, which compilers should have no difficulty recognizing. Since the authors of the Standard failed to explicitly require that the CIS guarantees be honored in useful fashion, however, the authors of clang and gcc have decided they'd rather claim that programs relying upon the decades-old CIS guarantees is "broken" than honor 40+ years of precedent.

Upvotes: 5

Related Questions