代表C中的动态类型

时间:2009-09-28 05:07:08

标签: c data-representation

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

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可能会在value之前存储class,当我在上面访问foo->class时,我实际上会访问foo->value,这显然很糟糕。便携性是一个很大的目标。

这种方法有其他选择:

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

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

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

这会创建一个重定向级别,这会降低速度。速度是这里的目标。

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

void* foo = bar();

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

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

  1. 隐藏,容易出错C.上面的代码只计算单成员偏移量;对于比整数更复杂的类型,它会变得更糟。我或许可以使用宏来缓解这种情况,但无论如何都会很痛苦。
  2. 由于没有struct表示对象,我没有堆栈分配选项(至少没有在堆上实现我自己的堆栈)。
  3. 基本上,我的问题是,如何在不付钱的情况下得到我想要的东西?有没有办法可移植,不同类型的大小不一致,不使用重定向,并保持我的代码漂亮?

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

6 个答案:

答案 0 :(得分:7)

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

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

(你不需要使用联盟来获取任何东西 - 它只需要在范围内)

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

  

[#5]有一个例外,如果是值   使用union对象的成员   当最近的商店到了   对象是一个不同的成员,   行为是实现定义的。   一个特殊的保证是按顺序   简化工会的使用:如果一个   union包含几个结构   共享一个共同的初始序列(见   下面),如果是对象   目前包含其中一个   结构,允许检查   其中任何一个的共同初始部分   声明的任何地方   完成的联盟类型是   可见。两种结构有共同之处   初始序列如果对应   成员有兼容的类型(和,   用于a的位字段,相同的宽度)   一个或多个初始序列   成员。

(这不是直接说它没关系,但我相信它确实保证如果两个struct有一个共同的初始序列并且被放入一个联合,它们将以同样的方式在记忆中布局 - 无论如何,它在很长一段时间内肯定是惯用的。

答案 1 :(得分:6)

请参阅Python PEP 3123(http://www.python.org/dev/peps/pep-3123/),了解Python如何使用标准C解决此问题.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*

答案 2 :(得分:3)

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

  

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

第6.7.2.1节也说:

  

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

     

[...]

     

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

这可以保证您的需求。

在你提出的问题中:

  

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

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

  

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

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

答案 3 :(得分:3)

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

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

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:最后,有一个纳米拳击,其中每个对象句柄只有64位。

double object;

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

答案 4 :(得分:2)

  

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

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

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

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

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

答案 5 :(得分:2)

我很欣赏这个问题和答案提出的迂腐问题,但我只想提一下,CPython已经“或多或少地永远地”使用了类似的技巧,而且它已经在各种各样的C编译器中运行了数十年。具体来说,请参阅object.h,像PyObject_HEAD这样的宏,像PyObject这样的结构:所有类型的Python对象(在C API级别下)正在获取指向它们的指针永远来回转换为/来自PyObject*而没有受到伤害。自从我上次使用ISO C标准演奏海洋律师已经有一段时间了,以至于我没有方便的副本(!),但我确实认为应该让它继续保持近20年的工作......