结构类型别名/ tagged-union没有联合

时间:2015-09-17 19:42:43

标签: c language-lawyer c11

对于两个(或更多)struct s:BaseSub共同的第一个(未命名的)struct,从{{转换/转换是否安全? 1}}到Base,反之亦然?

Sub

这些功能是否保证安全且技术上正确? (另外:零长度struct Base{ struct{ int id; // ... }; char data[]; // necessary? } struct Sub{ struct{ int id; // same '...' }; // actual data }; 成员是否必要且有用?)

char data[]

修改

我没有计划在struct Base * subToBase(struct Sub * s){ return (struct Base*)s; } struct Sub * baseToSub(struct Base * b){ if(b->id != SUB_ID){ return NULL; } return (struct Sub*)b; } 内进一步嵌套Base,而是可以在以后添加其他子类型(直接在Sub下),而无需更改{ {1}}。我主要担心的是Base的指针是否可以在Base和任何子项之间来回安全地转换。对(C11)标准的参考将是最受欢迎的。

修改v2

略微改变措辞以阻止OOP /继承讨论。我想要的是一个tagged-union,没有struct所以可以稍后扩展。我没有计划进行额外的嵌套。需要其他子类型功能的子类型可以明确地执行,而无需进行任何进一步的嵌套。

上下文

对于脚本解释器 1 ,我创建了一个伪面向对象的 tagged-union类型系统,没有Base。它有(抽象) 泛型基类型union,包含多个(特定)子类型,例如{{1} },unionObject等。每种类型 - String都有以下未命名的Number作为第一个成员:

List

struct标识对象的类型,struct#define OBJHEAD struct{ \ int id; \ int line; \ int column; \ } 应该(也)是不言自明的。各种对象的简化实现:

id

我知道最优雅且技术上最正确的做法是为line使用column,为各种子类型使用typedef struct Object{ OBJHEAD; char data[]; // necessary? } Object; typedef struct Number{ OBJHEAD; int value; // only int for simplicity } Number; typedef struct String{ OBJHEAD; size_t length; char * string; } String; typedef struct List{ OBJHEAD; size_t size; Object * elements; // may be any kind and mix of objects } List; Object * Number_toObject(Number * num){ return (Object*)num; } Number * Number_fromObject(Object * obj){ if(obj->type != TYPE_NUMBER){ return NULL; } return (Number*)obj; } 。但我希望类型系统是可扩展的(通过某种形式的类型注册表),以便以后可以添加类型而不会更改所有与enum相关的代码。

稍后/外部添加可能是:

id

无需更改union

这些转换是否保证安全?

(对于小的宏观滥用:Object当然会被广泛记录,因此额外的实施者将知道不使用哪些成员名称。想法不是隐藏标题,而是为了节省粘贴它每次都。)

3 个答案:

答案 0 :(得分:4)

允许将指向一种对象类型的指针转​​换为指向不同对象类型的指针(例如通过强制转换),但如果生成的指针未正确对齐,则行为未定义(C11 6.3.2.3/7) 。根据{{​​1}}和Base的成员以及依赖于实现的行为,转换为Sub的{​​{1}}未正确对齐的情况不一定如此。例如,给定......

Base *

......可能是实现允许Sub *个对象在32位边界上对齐,但要求struct Base{ struct{ int id; }; char data[]; // necessary? } struct Sub{ struct{ int id; }; long long int value; }; 个对象在64位边界上对齐,或者甚至在更严格的边界上对齐。

这些都不受Base是否具有灵活数组成员的影响。

通过强制转换不同类型的指针值获取一种类型的指针值是否安全是一个不同的问题。首先,C对实现如何选择布局结构的限制很少:成员必须按照声明的顺序布局,并且在第一个之前不得有任何填充,否则,实现具有自由统治。据我所知,在您的情况下,如果两个结构中的匿名Sub成员具有多个成员,则不要求它们以相同的方式布局。 (如果他们只有一个成员,那么为什么要使用一个不规则的结构呢?)假设Basestruct中匿名结构后面的第一个元素相同的偏移量开始也是不安全的。< / p>

在实践中,取消引用Base.data的结果可能没问题,您当然可以实施测试来验证。另外,如果您有Sub来自subToBase()的转换,那么将其转换回来的结果(例如通过Base *)保证与原始版本相同Sub *(C11 6.3.2.3/7再次)。在这种情况下,转换为baseToSub()并返回对将指针解除引用为Sub *的安全性没有影响。

另一方面,虽然我在标准中找不到它的引用但我不得不说Base *在一般情况下非常危险。如果实际上未指向Sub *的{​​{1}}转换为baseToSub()(其本身是允许的),则 可以安全地取消引用指向未与Base *共享的访问成员的指针。特别是,根据上面的声明,如果引用的对象实际上是Sub,那么声明Sub *绝不会阻止Base产生未定义的行为。

要避免所有未定义和实现定义的行为,您需要一种避免强制转换并确保一致布局的方法。 @ LoPiTaL建议在Base结构中嵌入一个类型Base.data结构,这是一个很好的方法。

答案 1 :(得分:2)

不,它不安全,至少在所有情况下都不安全。如果您的编译器看到具有不同基类型的两个指针pq,则可能始终假设它们不是别名,或者换句话说它可能总是假设{{1} }和*p是不同的对象。

你的演员在这个假设中打了一个洞。那就是你有一个功能

*q

允许优化器避免从内存中额外读取。

如果您知道没有任何功能可以通过不同类型的指针看到相同的对象,那么您将是安全的。但是这样的断言并不容易,所以你最好避免这样的骚扰。

答案 2 :(得分:1)

这是正确的,因为它保证在结构的第一个成员上具有相同的对齐方式,因此您可以从一个结构转换为另一个结构。

然而,实现你的行为的常用方法是继承&#34;继承&#34;基类:

//Base struct definition
typedef struct Base_{
    int id;
    // ...
    //char data[]; //This is not needed.
}Base;

//Subclass definition
typedef struct Sub_{
    Base base;  //Note: this is NOT a pointer
    // actual data
}Sub;

现在,您可以将Sub结构转换为Base结构,或者只返回已经是Base类型的第一个成员,因此不再需要进行强制转换。

提醒一句:不要滥用MACROS。 MACROS对许多事情都很好,但是滥用它们可能会导致难以阅读和维护代码。 在这种情况下,宏很容易被基础成员替换。

最后一句话,您的宏容易出错,因为现在隐藏了成员名称。最后,您可能会添加具有相同名称的新成员,并在不知道原因的情况下获得奇怪的错误。

当您进一步将层次结构扩展为子子类时,您将最终必须编写所有基类MACRO,而如果您使用&#34;继承&#34; aproach,你只需要写直接基数。

这些解决方案都没有真正解决您的问题:继承。您将拥有的唯一真正的解决方案(首选)是更改为纯粹的OO语言。由于与C的相似性,最佳匹配将是C ++,但可以使用任何其他语言。