65.9K
CodeProject 正在变化。 阅读更多。
Home

使用类型推断进行 C 语言编程

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.86/5 (7投票s)

2018 年 4 月 12 日

BSD

6分钟阅读

viewsIcon

15324

在本文中,我将介绍 PsycheC,一个用于 C 语言的类型推断引擎。

引言

[葡萄牙语版本 在此处 提供。]

C 编程语言已经存在了 40 年。多年来,C 在许多方面都得到了发展(C11 是当前的标准),但其基本特性保持不变:一种文本的 #inclusion 机制,一种命令式编程模型,作用域是词法界定的,并且,除其他事项外,表达式的类型是在编译时静态确定的。

C 的类型系统除了是静态的(这意味着类型检查在编译期间执行)之外,还有一些特性。

  • 它是 标称类型 的,这意味着 C 中类型的等价性由名称决定,这些名称在类型声明(例如 struct)中指定。这种方法与 结构类型系统 相反,后者依赖于类型的结构来确定其等价性。
  • 当我们声明一个 C 变量时,必须有一个类型明确地与之关联(例如,intdouble)。这与具有所谓 类型推断 的语言不同,在这些语言中,程序员可以免去标注类型的麻烦。
  • 尽管“弱类型”和“强类型”没有正式或普遍接受的定义,但由于 C 的类型系统的宽容性,我们通常将其归类为前者。这一点可以从涉及 void* 的隐式转换中看出。

对类型系统的完整讨论超出了本文的范围。特别是,与多态性及其变体(如 特设多态参数多态子类型)相关的主题。因此,我的目标仅限于讨论类型推断。准确地说,我想介绍一个支持此功能的 C“编译器”。使用此工具,您可以使用 C 编程,而无需为 structtypedef 编写声明。

使用 PsycheC 重构程序

在介绍 PsycheC 之前,值得区分全局类型推断和从孤立表达式进行的局部类型推断的概念。如今,许多语言提供了“自动推导”功能,其中变量可以在没有关联的类型说明符的情况下声明,前提是它带有初始化值。

void f () {auto v = 42; } // auto is the placeholder in C++
void g () {var v = 42; } // in C #, the placeholder is var

在上例的代码片段中,确实存在类型推断。但是,这是在受限的上下文中进行的。此功能的实现相对简单,它通过检查构成完整表达式的子表达式(例如,字面量或注解声明)的类型,并结合语言规则来完成。

我所指的类型推断是复杂的,可能涉及整个程序。在这种表述中,会收集一组表达式,但完整表达式不一定由带有类型注解声明的子表达式构成。最终,我们得到一个由许多表达式组成的约束系统。最终的问题是找到能够解决该系统的最通用的类型。这种类型被称为 主类型;具有此类类型系统的语言(称为 Hindley-Milner)的经典例子是 ML 和 Haskell。

一个问题出现了:是否有可能将高级类型推断集成到 C 的友好类型系统中?是的,这正是 PsycheC 所做的!而且这并非易事。C 的类型系统并非易于理解。尽管 C 语言不支持继承,但 void* 转换模仿了一种多态性;类型限定符带来了有趣的挑战——非 const 指针可以赋给 const 指针,但反之则不行;此外,在没有声明的情况下,C 语言在语法和语义上都变得模糊(例如,x * y 可能是一个指针声明或乘法,而 z = 0;可能是整数初始化或 NULL 指针)。

严格来说,PsycheC 并非编译器。它不依赖任何奇怪的语法,也不引入任何新关键字。它的目标是处理纯 C 语法。本质上,PsycheC 是一种能够“发现”程序中出现的、但缺少声明的表达式的主类型的分析工具。因此,PsycheC 是一种类型声明生成器:如果它找到一个名称 T 且其声明缺失,它会为 T 创建一个相应的 struct,或者让 T 成为现有类型的 typedef

int main()
{
    T v = 0;
    v->value = 3.14;
    v->next = v;
    return 0;
}

将上面代码片段的内容保存到文件 test.c 中,并尝试使用 gcc、clang 或您选择的任何 C 编译器进行编译。您将收到一个类似于下面所示的错误。这个错误是预期的,因为 T 的定义并未出现在程序中。

$ clang test.c
test.c:3:5: error: use of undeclared identifier 'T'
    T v = 0;
    ^

然而,如果 C 语言原生支持类型推断,编译器就能确定该程序的一种解决方案是:将 T 定义为一个指向 struct 的指针,该 struct 初始化为 0;向 T 添加一个名为 value、类型为 double 的字段;并向 T 添加一个名为 next、类型递归指向自身的字段。正是 PsycheC 工具提供了这种智能,它为该程序生成了以下声明。

typedef struct TYPE_2__ TYPE_1__ ;
struct TYPE_2__ { double value; struct TYPE_2__* next; } ;
typedef TYPE_1__* T ;

如果您想尝试一下,而无需克隆/构建 github 仓库,可以看看这个 在线界面

用例:PsycheC 实践

此时,您可能想知道为 C 构建类型推断引擎是否值得付出如此多的努力。嗯……这取决于。首先,构建 PsycheC 是一次伟大的冒险。无论如何,如果您喜欢更接近 Python 的编程风格,那么您可能会有一些乐趣。尽管如此,PsycheC 可以不仅仅是一个玩具。典型的用例如下:

  • 您想快速原型化一个算法,专注于其功能方面,而无需担心类型的表示方式。建议:尝试实现 归并排序 的功能版本,而无需声明任何 struct
  • 您正在处理一个遗留的、嵌入式的或跨平台的项目,其中类型仅为目标架构声明,但在模拟或测试程序的平台中无法编译。在这种情况下,PsycheC 可用于重构不兼容的头文件。
  • 您需要运行分析、调试或测试通过 bug 跟踪器提交的代码片段,但您无法访问或没有时间编译原始的整个程序。在某些情况下,类型存根足以重现问题。

从学术界到工业界

PsycheC 主要是一个学术项目。事实上,其背后的理论已在 POPL(编程语言原理)会议上发布,发布年份是 2018 年。要使用它,需要遵循一套“协议”任务,这可能有点麻烦。为了弥合这一差距,我们创建了 Cnippet 工具。

Cnippet 是 PsycheC 的一个封装器,它与 gcc 或 clang 集成,并抽象了上述过程。此外,Cnippet 理解部分标准库,并识别一些常见的宏(在各种平台上很常见)。要为您的 C 程序推断类型,只需调用 Cnippet,并将您会传递给实际 C 编译器的任何选项转发给它。

$ cnip clang -c test.c -o test.o

希望您喜欢这篇文章,并尝试一下 PsycheC。谢谢!

© . All rights reserved.