【问题标题】:Generate version ID of struct definition?生成结构定义的版本 ID?
【发布时间】:2012-06-27 21:37:15
【问题描述】:

基本上,我想要的是某种与结构的确切定义相关联的编译时生成的版本。如果结构的定义发生任何变化(字段添加、移动、可能重命名),我也希望该版本也发生变化。

在读取之前序列化的结构时,这样的版本常量会很有用,以确保它仍然兼容。另一种方法是手动跟踪手动指定的常量,如果忘记递增它(反序列化会产生垃圾),这可能会产生混乱的影响,并且还会提出何时准确递增它的问题(在开发和测试期间,或仅在某种情况下)释放)。

这可以通过使用外部工具在结构定义上生成散列来实现,但我想知道 C 编译器(和/或它的预处理器)本身是否有可能。

这实际上是某种形式的内省,所以我怀疑这在 ANSI C 中可能根本不可能,但我会很高兴有一个适用于 gcc 和 clang 的解决方案。

【问题讨论】:

    标签: c gcc clang


    【解决方案1】:

    Windows API 过去(现在仍然如此?)有一个 size 成员作为结构的第一个成员之一,因此它知道它正在传递的结构的版本(参见 WNDCLASSEX 作为示例):

    struct Foo
    {
        size_t size;
        char *bar;
        char *baz;
        /* Other fields */
    };
    

    在调用之前,您使用 sizeof 设置大小:

    struct Foo f;
    
    f.size = sizeof(struct Foo);
    f.bar = strdup("hi");
    f.baz = strdup("there");
    
    somefunc(&f);
    

    然后somefunc 会根据size 成员知道它正在处理哪个版本的结构。因为sizeof 是在编译时而不是运行时评估的,这允许向后兼容 ABI。

    【讨论】:

    • 谢谢。我考虑过只使用 sizeof,但这当然不能防止移动的字段或导致任何先前看到的大小的其他操作。对于 Windows API,将修改限制为仅添加字段可能是个好主意,但我目前真的不想走那么远......
    • @Julien 的想法是只添加新字段并弃用需要移动或修改的字段。问题是它不能跨不同的编译器/系统移植,因为填充和基本类型宽度是实现定义的。
    • @ouah 是的,我明白了,但正如我所说,我现在不想走“字段可能只被弃用”的路线。我也知道这不会是可移植的,但目前没有必要。 (尽管如此,一种可移植的方式是可以想象的,而且会更好。)
    【解决方案2】:

    没有什么可以自动完成,但是您可以构建一些可以合理可靠地工作的东西:您可以使用sizeofoffsetof,并将它们组合在一起,使组合它们的顺序很重要。这是一个例子:

    #include <stdio.h>
    #include <stddef.h>
    
    #define COMBINE2(a,b) ((a)*31+(b)*11)
    #define COMBINE3(a,b,c) COMBINE2(COMBINE2(a,b),c)
    #define COMBINE4(a,b,c,d) COMBINE2(COMBINE3(a,b,c),d)
    
    typedef struct A {
        int a1;
        char a2;
        float a3;
    } A;
    
    typedef struct B {
        int b1;
        char b2;
        double b3;
    } B;
    
    typedef struct C {
        char c2;
        int c1;
        float c3;
    } C;
    
    typedef struct D {
        int d1;
        char d2;
        float d3;
        int forgotten[2];
    } D;
    
    int main(void) {
        size_t aSign = COMBINE4(sizeof(A), offsetof(A,a1), offsetof(A,a2), offsetof(A,a3));
        size_t bSign = COMBINE4(sizeof(B), offsetof(B,b1), offsetof(B,b2), offsetof(B,b3));
        size_t cSign = COMBINE4(sizeof(C), offsetof(C,c1), offsetof(C,c2), offsetof(C,c3));
        size_t dSign = COMBINE4(sizeof(D), offsetof(D,d1), offsetof(D,d2), offsetof(D,d3));
        printf("%ld %ld %ld %ld", aSign, bSign, cSign, dSign);
        return 0;
    }
    

    此代码prints

    358944 478108 399864 597272
    

    如您所见,此代码为每个结构生成运行时常量,这些常量对不同长度的字段重新排序和更改字段类型做出反应。即使您忘记更新计算所依据的字段列表,它也会对添加字段做出反应,这应该会产生某种安全网。

    【讨论】:

    • 啊,我忘了offsetof。我想这是最接近 ANSI C 的东西?
    • @Julien 是的,我认为至少在 ANSI C 中,将这两者结合起来再好不过了。不过,您可以构建更好的COMBINE:我的看起来又快又脏工作,虽然我在生产代码中使用类似的东西来散列各种结构。
    • @Julien 如果您没有必须在编译时计算函数的限制,您可以通过计算表示结构字段名称的字符串的哈希值来防止重命名(您需要使用#预处理器字符串化操作符)。
    • 可惜......虽然它可能值得一个 gcc/clang 扩展 8)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-13
    • 1970-01-01
    • 1970-01-01
    • 2017-08-25
    • 2013-01-11
    相关资源
    最近更新 更多