首页 / 资讯中心 / 文章详情

关于C++ TpeScript系列的泛型

关于C++ TpeScript系列的泛型 ★ FEATURED ARTICLE
前言先纠正标题里的一个拼写问题「TpeScript」应为「TypeScript」。这个标题想讨论的是 C 模板与 TypeScript 泛型之间的关系。两者都允许把类型作为参数写出「对多种类型都适用」的代码但它们的实现模型几乎相反C 模板在编译期为每一组实参生成一份独立代码TypeScript 泛型只存在于类型检查阶段编译成 JavaScript 后类型参数被完全擦除。由此产生的误解很典型。有人以为 TypeScript 的泛型和 C 模板一样可以在运行时new T()、判断x instanceof T实际上泛型信息在运行期不存在。也有人以为 C 的模板只是「带类型检查的宏」忽略了它能做编译期计算、模板特化和重载决议。还有人把 TypeScript 的extends当成 C 的继承其实它只是对类型参数施加结构约束。本文分别说明两种泛型的机制再从实例化方式、约束表达、特化能力、运行期行为四个维度做对照。C 部分以 C17 为基准涉及 C20 的约束时单独标注TypeScript 部分以 4.x 及以上的常见语义为准。一、C 模板编译期实例化C 的类模板与函数模板是「代码生成器」。编译器看到larger(3, 7)时用T int实例化一份函数看到largerstd::string(...)时再生成一份T std::string的版本。这个过程叫单态化monomorphization每份实例都是真正的机器码调用时没有运行期类型分派通常也能被内联。// 适用于 C17#include iostream#include string#include utilitytemplate class TT larger(const T a, const T b) {return a b ? b : a;}template class Tstruct Box {T value;explicit Box(T v) : value(std::move(v)) {}};int main() {std::cout larger(3, 7) \n;std::cout largerstd::string(abc, abd) \n;Boxint b{42};std::cout b.value \n;return 0;}模板的参数推导基于实参类型。larger(3, 7)推导出T int如果写larger(3, 7.0)两个实参类型不同推导会失败需要显式指定largerdouble(3, 7.0)或者把模板写成两个类型参数。这一点与 TypeScript 的类型推断不同TS 会在多个候选类型之间做联合或最佳公共类型C 的推导规则更严格冲突时直接报错。模板定义通常必须放在头文件里。因为编译器在实例化时需要有完整的函数体如果把定义放进.cpp另一个翻译单元只看到声明链接阶段就找不到对应实例报未定义引用。这不是工具链的缺陷而是「按需实例化」模型的必然结果。C11 的extern template可以抑制某些实例的重复生成但不会改变「定义必须可见」这一前提。C20 引入了概念concepts把对类型参数的约束从「实例化失败时的长篇报错」变成显式声明// 需要 C20 及以上#include conceptstemplate std::totally_ordered TT larger(const T a, const T b) {return a b ? b : a;}std::totally_ordered要求类型支持全序比较。不满足约束的类型在重载决议阶段就被排除错误信息指向约束本身而不是模板展开后的某一行。概念是 C20 特性GCC、Clang、MSVC 的支持版本不同使用前要确认工具链。模板还支持全特化与偏特化。例如可以为Boxchar const*写一个专门版本改变其存储方式函数模板支持重载与全特化但不支持偏特化常用重载或类模板偏特化来替代。这种「针对具体类型改写实现」的能力是 TypeScript 泛型没有的。二、TypeScript 泛型类型擦除与结构约束TypeScript 的泛型写在类型层面作用是让调用方与实现方共享同一个类型变量从而获得类型检查与推断。它不会为每个类型参数生成不同代码编译产物里泛型参数被擦除运行时只有一个函数实现。// 适用于 TypeScript 4.x 及以上function largerT extends number(a: T, b: T): T {return a b ? a : b;}interface BoxT {value: T;}const b: Boxnumber { value: 42 };const n larger(3, 7); // T 推断为 numberextends在这里不是继承而是约束T必须可赋值给number。TypeScript 采用结构类型系统判断「是不是」看成员形状而不是看声明关系。因此约束可以写成T extends { id: number }任何含有id: number的类型都满足不需要显式实现某个接口。更复杂的约束常与keyof配合// 适用于 TypeScript 4.x 及以上function getPropertyT, K extends keyof T(obj: T, key: K): T[K] {return obj[key];}const person { name: Ada, age: 36 };const name getProperty(person, name); // 类型为 stringK extends keyof T把键限制为T的成员名返回类型T[K]则精确到该键对应的值类型。这套表达在 C 里没有直接对应物C 的模板参数是类型或非类型值不能直接对「对象的成员名」做约束需要借助 SFINAE、decltype或概念手工搭建。TypeScript 泛型还有默认类型参数T string、泛型约束的组合、条件类型与映射类型等能力。这些都属于类型层面的运算编译后不保留。运行时无法判断一个值是不是Boxnumber也无法根据T做不同的分支——因为T已经不存在了。三、四个维度的对照维度C 模板TypeScript 泛型实例化时机编译期按实参生成多份代码仅类型检查运行期擦除运行期类型保留每份实例是独立函数不保留只有一个实现约束方式SFINAE、static_assert、C20 概念extends结构约束特化全特化、类模板偏特化无特化用条件类型模拟代码体积可能膨胀每种类型一份不随类型参数增加类型检查实例化时检查模板体调用点与实现分别检查编译错误展开后可能很长概念可改善通常指向具体的类型不匹配典型用途零开销抽象、容器、编译期计算复用类型关系、API 类型安全这张表里最容易被忽视的是「代码体积」与「运行期行为」的取舍。C 为每种类型生成专门代码换来的是内联、常量折叠和针对该类型的优化代价是二进制变大、编译变慢。TypeScript 只有一份实现体积不随类型增长但无法针对具体类型做运行时特化也不能在泛型函数里创建T的实例。另一个差异是「约束到底约束了什么」。C 概念约束的是「这个类型能否完成模板体中用到的操作」检查发生在实例化时或概念的约束满足检查时TypeScript 的extends约束的是「这个类型能否赋值给目标类型」遵循结构兼容规则。两者都能提前发现错误但判断依据不同前者看操作是否合法后者看形状是否兼容。常见坑点坑点一以为 TypeScript 泛型在运行时可用。❌function makeT() { return new T(); }或if (x instanceof T)泛型参数在运行期不存在。✅ 把构造函数或类型守卫作为参数传入例如接收ctor: new () T或判定函数。坑点二把 C 模板定义放进.cpp。❌ 头文件只留声明定义在源文件里其他翻译单元实例化时报未定义引用。✅ 模板定义放头文件确实要分离时在源文件中显式实例化需要的类型。坑点三以为 TypeScript 的extends是继承。❌ 认为T extends Base要求T真的继承Base类。✅ 它表示结构可赋值只要形状匹配即可必要时用implements或判别字段表达意图。坑点四C 模板参数推导冲突时硬写转换。❌larger(3, 7.0)推导失败后把函数改成接收两个不同类型却仍按T返回导致截断。✅ 显式指定largerdouble(3, 7.0)或把模板设计成两个类型参数并明确返回类型。坑点五用any绕过 TypeScript 泛型约束。❌function fT(x: any): T { return x; }泛型形同虚设调用方拿到错误的类型承诺。✅ 用unknown接收外部输入缩小后再返回保持泛型参数与实现的一致性。坑点六认为 C 模板一定会被内联、一定零开销。❌ 在头文件里堆大量模板假设编译器必然展开优化。✅ 模板是否内联、是否消除抽象取决于优化设置与具体调用以生成的代码和测量为准。坑点七在 TypeScript 泛型里做运行时校验的假设。❌ 接口声明Boxnumber后就认为运行时一定是数字外部 JSON 直接断言。✅ 在边界处做运行时校验类型断言不能替代数据验证。坑点八C 模板报错时只看最后一行。❌ 看到「no matching function」就改调用点忽略前面「模板体里某个操作不合法」的根因。✅ 从第一条与模板实例相关的错误读起或用概念把约束前置。总结问题CTypeScript泛型是什么编译期代码生成机制类型层面的参数化何时消失不消失生成具体实例编译后擦除如何约束概念、SFINAE、static_assertextends结构约束能否特化可以不能用条件类型模拟运行期能否建 T可以T{}、new T不可以代码体积随实例增加基本不变错误发现实例化时类型检查时C 模板与 TypeScript 泛型共享「类型参数」这个表面形式底层却是两套哲学前者用代码复制换取运行期效率与特化能力后者用类型擦除换取实现唯一与编译产物体积。理解这一点就不会把 TS 的泛型当成「不用写类型的模板」也不会期待 C 模板具备运行期反射。选择哪种取决于你要的是零开销的具体代码还是灵活的类型关系描述。
阅读完成 · 觉得有帮助?
咨询建站