多个结构,需要在方法中访问的相同字段

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

我目前正在尝试用 C 编写一些简单的文字控制台游戏来获得乐趣。

为此,我需要能够打印 window 类结构......好吧...... C.

我想使用通用渲染方法(我们称之为 frame_render(...))来渲染所有不同的 "ui elements"

现在的问题是:如何解决?

给定场景:

// 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; ... }

我如何确保我的 xywidthheight 始终处于正确的内存位置? "just" 一开始就把它们按相同的顺序排列就够了吗?

另外,如何设计方法头来接受它?

"classical" 方法是有一个 struct,其中包含一个 union 所有可能的对象,以及一个 enum 来标识已传递的确切对象:

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

将此结构传递给渲染器后,在 type 字段上使用 switch 来提取坐标等:

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

据报道,正是这种怪诞行为促使 Stroustrup 发明了 C++ :)

已编辑

另一种 "classical" 解决方案是有一个单独的 struct,它具有任何对象的尺寸和位置:

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

您可以将此结构存储在任何特定对象的开头 struct 并使用强制转换来访问它:

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

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

在渲染器中:

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

后一种方法可能有些不安全。为了使其 100% 安全,将几何体与特定于对象的信息分开,并将它们作为两个单独的结构传递给渲染器。

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?

有多种方法可以做到这一点。

这是一个示例,使用 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;
    }
}

这里有一个使用虚函数的方法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);
}

这是使用 union 的第三种方式。它更简单,但要求基本结构与最大的子结构一样大-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;
    }
}

更新:

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

如果使用 derived 类型(例如 framebutton)进行分配,free 没有问题,但是 不是基类型geomalloc(sizeof(struct button)).

要获得 简单 数组 [形状],需要使用 union 方法(即所有派生结构必须具有相同的大小)。但是,如果我们有一些使用 lot 比其他 space 多的子类型,这将是浪费:

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

这个可以仍然可以用方法#1或#2[其中子类型结构的大小不同]和间接来完成指针数组:

void
all_render(struct geo **glist,int ngeo)
{

    for (;  ngeo > 0;  --ngeo, ++glist)
        any_render(*glist);
}

我会考虑[双向] 链表,而不是不同形状的数组。这允许子类型结构具有不同的大小。我们将向 struct geo 添加一个 struct geo *next 元素。然后,我们可以这样做:

void
all_render(struct geo *geo)
{

    for (;  geo != NULL;  geo = geo->next)
        any_render(geo);
}

列表方法可能更可取,特别是如果我们 add/remove 在动态基础上构建形状 [或根据 Z 深度重新排序它们]。

或者,某些形状可能 包含 其他形状。因此,我们可以将 struct geo *children 添加到 struct geo。然后,很容易(例如)绘制一个包含框,然后通过遍历 children 列表绘制该框内的所有形状。如果我们选择 children,我们也可以添加 struct parent *parent,以便每个形状都知道包含它的形状。

如果所有结构都以相同类型的成员开始,以相同的顺序,相应的成员将具有相同的偏移量。大多数编译器都可以配置为允许使用任何结构类型的指针来检查任何其他公共初始序列的成员,但存在一些问题:

  1. 在一些不常见的平台上,如果对象后跟填充字节,则将对象和填充字节一起写入的指令(可能在后者中存储无意义的值)可能比写入的指令更快只有对象。如果某个成员在某些结构中后跟一个填充字节,但在另一个结构中跟一个有意义的数据,则使用后跟填充字节的类型对该成员进行写入可能会用无意义的值覆盖 "padding bytes",从而破坏其他结构类型中以下成员的值。我不知道当前使用的任何体系结构对于除位域以外的任何结构成员来说都是一个问题,而且我不知道任何当前的实现即使对于那些也是一个问题,但这种可能性可能会出现在某些平台上,特别是位域。

  2. 给定如下:

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

    一些编译器,如 gcc 和 clang 不会可靠地考虑到函数返回的值可能取决于 struct s2 类型对象的公共初始序列的一部分所持有的值的可能性调用时间,除非优化被禁用(例如使用 -fno-strict-aliasing 选项)。我认为在函数调用表达式中存在从 struct s2*struct s1* 的转换应该允许高质量的编译器识别任何函数可能对类型 struct s1 的对象做某事可能是在 struct s2 上完成,但由于标准没有明确要求,gcc 和 clang 的作者拒绝做出任何努力来可靠地识别此类结构,即使在上述简单的情况下也是如此。

使用公共初始序列规则的代码几乎可以在任何适当配置的编译器上可靠地工作,但是像 gcc 和 clang 之类的代码必须使用 -fno-strict-aliasing 选项进行特殊配置。自 1974 年以来,利用公共初始序列保证的能力一直是该语言的一个公认部分,并且在编写标准时,任何熟悉该语言的人都会理解它被设计为允许像上面那样的构造,编译器应该不难识别。由于该标准的作者未能明确要求以有用的方式兑现 CIS 保证,因此 clang 和 gcc 的作者决定他们宁愿声称依赖已有数十年历史的 CIS 保证的程序 "broken"比荣誉40+年的先例。