【问题标题】:Multiple structs, same fields that need to be accessed in a method多个结构,需要在一个方法中访问的相同字段
【发布时间】:2018-10-10 22:05:43
【问题描述】:

我目前尝试用 C 编写一些有趣的小游戏。

为此,我需要能够在……好吧……C中打印类似窗口的结构。

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

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

给定场景:

// 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 始终处于正确的记忆点? 一开始就“只是”将它们按相同的顺序排列就足够了吗?

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

【问题讨论】:

  • 如何将结构传递给方法?通过void*?
  • 计划是或多或少地将它们传递给frame_render(frame*)。他们将坐在一个通过 for 循环迭代的列表中

标签: c struct generic-programming


【解决方案1】:

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

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

  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 保证的程序是“损坏的”,而不是向 40 多年的先例致敬。

【讨论】:

  • @X39:我认为标准的作者显然希望它能够工作,并且在实践中,只要您确定在需要它的编译器上使用适当的标志,它就会工作。我所知道的唯一两个需要这样一个标志才能利用 CIS 保证的编译器是 clang 和 gcc,但可能还有其他编译器。请注意,clang 和 gcc 肯定 支持没有标志的此类代码。
  • @x39:编写有意义的代码并记录使其工作所需的内容。该标准并未尝试完全指定一种对任何特定目的有用的语言,并且启用了最大优化的 clang 和 gcc 处理的方言对于一些不涉及处理来自未知来源的输入的特殊目的可能非常有用,但是真的不适合大多数人。与其弯腰用不合适的方言编写代码,不如在适当的时候使用“流行的扩展”——只需证明你正在这样做。
  • @X39:顺便说一句,我忘了提一个细节:了解restrict 限定符,并在适当的时候使用它。正确使用该限定符可以重新启用大多数被-fno-strict-aliasing 阻止的有用优化,除此之外还有更多。我觉得奇怪的是,直到restrict 使它们在很大程度上变得不必要之后,编译器才真正积极地使用类型访问规则。
  • @X39:在某些情况下,使用“基本类型”作为前缀的方法可以工作,但根据对象大小和对齐方式,它最终可能需要许多结构包含其他不必要的填充。这种方法也可能与未来正在探索以纳入标准的“指针来源”概念相冲突。鉴于struct foo {struct bar b; int q; ...} f;,编译器将受益于知道对doSomething(&f.b); 的调用是否会影响f.q,我认为推动指针出处的人们希望允许编译器假设它不会。
  • @X39:如果程序员编写了doSomething((struct s1*)struct2Ptr);,那么关注这些事情的编译器将拥有识别指针接收者可能将其作为struct s1 处理所需的所有信息或struct s2。如果程序员编写doSomething(&struct2Ptr->header);,编译器将不知道doSomething 是否会将其操作限制在标头,或者可能访问所有.我相信为编译器提供安全有效优化所需的信息,而仅传递标头指针则不会。
【解决方案2】:

一开始就“只是”将它们按相同的顺序排列就足够了吗?

是的,如果你像上面所做的那样小心的话。

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

有不同的方法可以做到这一点。

下面是一个示例,使用 [ugly] 等效的 c++“基”类:

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

下面是一个使用虚函数表的方法:

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 的第三种方式。它更简单,但要求基础结构与最大的子类一样大:

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

更新:

这种方法铸造安全吗?例如。将所有内容放入frame* 类型的数组中,然后访问frame->geo?或者这会导致以后调用free(..) 时出现任何问题?

如果分配是使用派生类型(例如framebutton)完成的,free 没有问题,但不是基本类型geomalloc(sizeof(struct button)).

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

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);
}

列表方法可能更可取,特别是如果我们动态添加/删除形状[或根据 Z 深度重新排序它们]。

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

【讨论】:

  • 这种方法铸造安全吗?例如。将所有内容放入frame* 类型的数组中,然后访问frame->geo?或者这会导致以后调用free(..)时出现任何问题?`
【解决方案3】:

“经典”方法是拥有一个包含所有可能对象的unionstruct 和一个标识已传递的确切对象的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++ :)

已编辑

另一个“经典”解决方案是有一个单独的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% 安全,请将几何体与特定于对象的信息分开,并将它们作为两个单独的结构传递给渲染器。

【讨论】:

  • 我有点喜欢这种方法,不过......它要求我的renderable(或frame)结构中的所有类型都是已知的
  • 如果你不知道所有的类型,你会如何渲染它们?
  • 它们都被渲染成相同的 :) 那是真正有趣的部分.. 上面的场景只是一些分解的例子.. 差异很小,主要与它们的实际使用方式有关,而不是与完全渲染
  • 根据 supercat 发布的内容,这在某些编译器(例如 gcc)上可能存在问题。认为我会接受 Craig Estey 的答案,仅仅是因为时机和“完整性”。不过,请感谢 :)
  • 经典方法实际上是使用指向任何合适结构类型的指针;一个著名的例子是 SOCK_ADDR 类。我在那个时代看到的代码很少为此目的使用联合。不幸的是,当编译器作者询问标准的作者是否要求编译器即使在无用的情况下也支持 CIS 保证时,标准的作者同意没有必要让标准强制它,认为人们寻求编写高质量的编译器会在它们有用的情况下支持它们,无论......
猜你喜欢
  • 1970-01-01
  • 2014-07-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多