代表C中的动态类型

我正在写一种动态类型的语言。 目前,我的对象以这种方式表示:

struct Class { struct Class* class; struct Object* (*get)(struct Object*,struct Object*); }; struct Integer { struct Class* class; int value; }; struct Object { struct Class* class; }; struct String { struct Class* class; size_t length; char* characters; }; 

目标是我应该能够将所有内容作为struct Object*传递,然后通过比较class属性来发现对象的类型。 例如,要转换一个整数以供使用,我只需执行以下操作(假设integer的类型为struct Class* ):

 struct Object* foo = bar(); // increment foo if(foo->class == integer) ((struct Integer*)foo)->value++; else handleTypeError(); 

问题是,据我所知,C标准没有对如何存储结构做出任何承诺。 在我的平台上这是有效的。 但是在另一个平台上, struct String可能会在class之前存储value ,当我在上面访问foo->class时,我实际上会访问foo->value ,这显然很糟糕。 便携性是这里的一个重要目标。

这种方法有其他选择:

 struct Object { struct Class* class; union Value { struct Class c; int i; struct String s; } value; }; 

这里的问题是union使用的空间与可以存储在union中的最大东西的大小相同。 鉴于我的一些类型是我的其他类型的很多倍,这意味着我的小类型( int )将占用与我的大类型( map )一样多的空间,这是一个不可接受的权衡。

 struct Object { struct Class* class; void* value; }; 

这会创建一个重定向级别,从而减慢速度。 速度是这里的目标。

最后的选择是传递void* s并自己管理结构的内部。 例如,要实现上面提到的类型测试:

 void* foo = bar(); // increment foo if(*((struct Class*) foo) == integer) (*((int*)(foo + sizeof(struct Class*))))++; else handleTypeError(); 

这给了我想要的一切(便携性,不同类型的不同尺寸等),但至少有两个缺点:

  1. 隐藏,容易出错C.上面的代码只计算单成员偏移量; 对于比整数更复杂的类型,它会变得更糟。 我可能能够使用宏来缓解这一点,但无论如何都会很痛苦。
  2. 由于没有表示对象的struct ,我没有堆栈分配选项(至少没有在堆上实现我自己的堆栈)。

基本上,我的问题是,如何在不付钱的情况下得到我想要的东西? 有没有办法可移植,不同类型的大小不一致,不使用重定向,并保持我的代码漂亮?

编辑:这是我收到的SO问题的最佳回复。 选择答案很难。 所以我只能选择一个答案,所以我选择了一个引导我解决问题的答案,但你们都收到了赞成票。

有关Python如何使用标准C解决此问题,请参阅Python PEP 3123( http://www.python.org/dev/peps/pep-3123/ ).Python解决方案可以直接应用于您的问题。 基本上你想要这样做:

 struct Object { struct Class* class; }; struct Integer { struct Object object; int value; }; struct String { struct Object object; size_t length; char* characters; }; 

如果您知道对象是整数,则可以安全地将Integer*Object* ,将Object*Integer*

C为您提供足够的保证,您的第一种方法将起作用。 您需要做的唯一修改是,为了使指针别名正常,您必须在范围内包含一个union ,该union包含您在其间投射的所有struct

 union allow_aliasing { struct Class class; struct Object object; struct Integer integer; struct String string; }; 

(你不需要使用联合任何东西 – 它只需要在范围内)

我相信标准的相关部分是这样的:

[#5]除了一个例外,如果在对象的最新存储是另一个成员时使用了union对象的成员的值,则该行为是实现定义的。 为了简化联合的使用,我们做了一个特别的保证:如果一个联合包含几个共享一个共同初始序列的结构(见下文),并且如果联合对象当前包含这些结构中的一个,则允许检查公共其中任何一个的初始部分都可以看到完整类型的联合声明。 如果对应的成员具有一个或多个初始成员的序列的兼容类型(并且对于位字段,具有相同的宽度),则两个结构共享共同的初始序列。

(这并没有直接说它没关系,但我相信它确实保证了如果两个struct有一个共同的初始序列并且一起被放入一个联合,它们将以相同的方式被放置在内存中 – 它肯定是无论如何,惯用C很长一段时间来承担这一点。

ISO 9899:1999(C99标准)第6.2.5节说:

结构类型描述了顺序分配的非空成员对象集(并且在某些情况下,是不完整的数组),每个成员对象具有可选的指定名称和可能不同的类型。

第6.7.2.1节还说:

如6.2.5中所讨论的,结构是由一系列成员组成的类型,其存储以有序序列分配,而union是由存储重叠的成员序列组成的类型。

[…]

在结构对象中,非位字段成员和位字所在的单元具有按声明顺序增加的地址。 指向适当转换的结构对象的指针指向其初始成员(或者如果该成员是位字段,则指向它所在的单元),反之亦然。 结构对象中可能存在未命名的填充,但不是在其开头。

这保证了您的需求。

在你提出的问题中:

问题是,据我所知,C标准没有对如何存储结构做出任何承诺。 在我的平台上这是有效的。

这适用于所有平台。 这也意味着您的第一个选择 – 您目前使用的是 – 足够安全。

但是在另一个平台上,struct String Integer可能会在class之前存储值,当我在上面访问foo-> class时,我实际上会访问foo-> value,这显然很糟糕。 便携性是这里的一个重要目标。

不允许兼容的编译器这样做。 [ 我用Integer替换了String,假设你指的是第一组声明。 仔细研究一下,你可能一直在指的是带有嵌入式联合的结构。 仍然不允许编译器重新排序classvalue ]

问题是,据我所知,C标准没有对如何存储结构做出任何承诺。 在我的平台上这是有效的。 但是在另一个平台上, struct String可能会在class之前存储value ,当我在上面访问foo->class时,我实际上会访问foo->value ,这显然很糟糕。 便携性是这里的一个重要目标。

我相信你在这里错了。 首先,因为您的struct String没有value成员。 其次,因为我相信C 确实保证了struct的成员在内存中的布局。 这就是为什么以下是不同的大小:

 struct { short a; char b; char c; } struct { char a; short b; char c; } 

如果C不做任何保证,那么编译器可能会优化这两个大小相同。 但它保证了结构的内部布局,因此自然对齐规则启动并使第二个大于第一个。

我很欣赏这个问题和答案提出的迂腐问题,但我只想提一下CPython使用类似的技巧“或多或少永远”,它已经在各种C编译器中运行了数十年。 具体来说,请参阅object.h ,像PyObject_HEAD这样的宏,像PyObject_HEAD结构:所有类型的Python对象(在C API级别下)都会获得指向它们的指针,这些指针永远地来回传递给PyObject* ,而不会造成任何伤害。 自从我上次使用ISO C标准演奏海洋律师已经有一段时间了,以至于我没有方便的副本(!),但我相信那里有一些限制, 应该让它继续工作,因为它有将近20年……

实现动态类型有三种主要方法,哪种方法最好取决于具体情况。

1)C风格的inheritance:第一个显示在Josh Haberman的回答中。 我们使用经典的C风格inheritance创建一个类型层次结构:

 struct Object { struct Class* class; }; struct Integer { struct Object object; int value; }; struct String { struct Object object; size_t length; char* characters; }; 

具有动态类型参数的函数将它们作为Object*接收,检查class成员,并根据需要进行强制转换。 检查类型的成本是两个指针跳跃。 获取基础值的成本是一个指针跃点。 在像这样的方法中,对象通常在堆上分配,因为在编译时对象的大小是未知的。 由于大多数`malloc实现一次至少分配32个字节,因此使用这种方法小对象可能会浪费大量内存。

2)标记联合:我们可以使用“短字符串优化”/“小对象优化”来删除访问小对象的间接级别:

 struct Object { struct Class* class; union { // fundamental C types or other small types of interest bool as_bool; int as_int; // [...] // object pointer for large types (or actual pointer values) void* as_ptr; }; }; 

具有动态类型参数的函数将它们作为Object接收,检查class成员,并根据需要读取union。 检查类型的成本是一个指针跃点。 如果类型是特殊小类型之一,则它直接存储在union中,并且没有间接检索值。 否则,需要一个指针跃点来检索该值。 这种方法有时可以避免在堆上分配对象。 虽然在编译时仍然不知道对象的确切大小,但我们现在知道容纳小对象所需的大小和对齐(我们的union )。

在前两个解决方案中,如果我们在编译时知道所有可能的类型,我们可以使用整数类型而不是指针来编码类型,并通过一个指针跳减少类型检查间接。

3)Nan-boxing:最后,有一个nan-boxing,其中每个对象句柄只有64位。

 double object; 

对应于非NaN double任何值被理解为简单的double 。 所有其他对象句柄都是NaN。 实际上,在常用的IEEE-754浮点标准中,实际上存在大量的双精度浮点数位,这些浮点数对应于NaN。 在NaNs的空间中,我们使用几个位来标记类型和数据的剩余位。 通过利用大多数64位机器实际上只有48位地址空间的事实,我们甚至可以在NaN中存储指针。 这种方法不会引起间接或额外的内存使用,但会限制我们的小对象类型,很尴尬,理论上不可移植C.