C 语言的类型擦除泛型函数:一个适度的非提案

今年早些时候,作者阅读了 Martin Uecker 的提案 N3212,提议为 C 语言添加参数化多态性。C++已有模板,许多现代系统语言也有某种形式的泛型编程支持,但此提案让作者思考有机会做出与其他语言不同且有意义、有用的东西。
C++模板依赖单态化,编译器为每个使用的类型集生成不同的特化,但这意味着更复杂的编译和链接模型。而 C 语言中一个函数定义通常在目标文件中对应一个符号,无需在编译后访问函数体,应保留这些特性。
Swift 及 N3212 提议的在系统语言中实现参数化多态性的路径是使用运行时元数据动态处理泛型类型,如 N3212 提议的新_Type 原始类型。指定语言集成的元数据系统面临的挑战是要提供足够信息满足语言支持的所有可能用途,又不能因过多不必要的元数据而使代码生成膨胀。
作者探讨了第三种更简单理解和实现、更灵活且更符合“C 语言精神”的方法,即类型擦除泛型结合默认参数机制在调用点收集适当的元数据,可提供大量表达性,便于为现有 C 接口添加更好的类型检查。

免责声明

作者表示其语言设计工作重点不在此,不会很快正式向 C 工作组提议,只是描述设计方向的大致框架,若能启发他人实施或提议则很好。同时解释思考为 C 增加新特性的原因,希望看到更多不同的系统语言。

泛型函数

为使通用函数的单个编译版本能处理任何输入类型,函数不能依赖特定类型的具体布局,C 中的不完全类型与此类似,通用类型参数也像不完全类型,如 void good_generic«T»(const T a, T b) 可,但 void bad_generic«T»(T a) 错误。通过传递匹配指针参数类型的函数指针和指针可实现有用的泛型函数,如 each_node«Node» 函数遍历链表。

默认参数用于收集类型元数据

要对泛型值或数据结构进行更多操作,需要更多类型元数据,在每个调用点显式传递大量元数据会很繁琐且影响泛型的安全性,通用函数声明可指定默认参数,如 void reduce«T»(T out, const T array, size_t count, void (sum)(T out, const T *element), size_t element_size = sizeof(T)) ,可自动从调用点收集相关元数据。

为现有函数添加泛型

利用上述两个特性,可将一些现有 C 函数改造为利用泛型,如对 qsort 函数进行改造,使其成为通用函数 void qsort«T[width]»(T base, size_t nel, size_t width = sizeof(T), int (compar)(const T , const T )) ,为保持兼容性,调用通用函数时未提供泛型参数则默认为 void ,但这样可能会影响类型安全性。

泛型结构体

用户定义的类型也可参数化,约束与函数参数类似,类型参数像不完全类型且不影响类型布局,如 struct hashable«Key» 结构体可封装类型的相关信息,不完全类型也可携带泛型参数提供接口边界的类型安全。

泛型联合和枚举

枚举和联合可表示替代,借鉴函数式语言中的 GADTs 思想,它们不仅可泛型化,还可限制某些成员仅对特定泛型参数可用,如 enum json_tag«T» 和 struct json«T» ,可实现更类型安全的标记联合。

参数化全局变量?

默认参数机制可用于从 sizeof 和 alignof 等内置操作填充元数据参数,但 C 语言目前没有提供类型和值之间的开放、用户定义的关联方式,可考虑声明全局变量名为参数化,如 _Generic const struct hashable«T» hashable_impl«T»; ,为哈希表 API 提供默认值,但仍会类型擦除,无法在运行时动态查找关联。

结论

这或许是一个不成熟的想法,但展示了一种与普通 C 的良好实现特性不冲突的泛型设计潜力,可在无需额外运行时支持的情况下实现单独编译。

阅读 10
0 条评论