深入浅出C++模板:让代码“通用化”的黑魔法

深入浅出C++模板:让代码“通用化”的黑魔法 在C开发中你一定遇到过这样的场景需要写一个求和函数既要支持int整型又要支持float浮点型甚至还要支持double双精度型。如果为每一种类型都写一个重载函数代码会变得冗余又难维护。而C模板就是解决这类问题的终极方案——它能让你写一套代码适配多种数据类型实现代码的通用化、复用化是C泛型编程的核心基石。今天我们就用通俗易懂的方式彻底搞懂C模板的原理、用法和实战场景。一、什么是C模板先给一个直白的定义C模板是创建通用代码的工具它允许程序员编写与类型无关的代码。编译器会在使用模板时根据传入的实际类型自动生成对应类型的代码。你可以把模板理解为代码的“模具”模具本身是通用的模板倒入不同的原料不同数据类型就能生产出不同的产品针对具体类型的代码。模板分为两大类函数模板通用函数适配多种参数类型类模板通用类适配多种成员变量/方法类型二、函数模板通用函数的实现1. 为什么需要函数模板先看没有模板的痛点// 整型求和intadd(inta,intb){returnab;}// 浮点型求和floatadd(floata,floatb){returnab;}// 双精度求和doubleadd(doublea,doubleb){returnab;}三个函数逻辑完全一样只是类型不同冗余度极高。2. 函数模板语法函数模板的核心是模板参数列表用template typename T声明templatetypename类型参数// 模板声明返回值类型 函数名(参数列表){// 函数体}template关键字声明这是一个模板typename关键字声明后面的T是类型参数也可以用class效果完全一样T类型参数自定义名称常用T、U、V代表通用类型3. 实战写一个通用求和函数模板#includeiostreamusingnamespacestd;// 函数模板声明templatetypenameTTadd(T a,T b){returnab;}intmain(){// 编译器自动推导类型intcoutadd(10,20)endl;// 编译器自动推导类型doublecoutadd(3.14,2.86)endl;// 显式指定类型推荐规范写法coutaddfloat(1.5f,2.5f)endl;return0;}运行结果30 6 44. 核心原理编译器看到add(10,20)时会自动用int替换T生成intadd(inta,intb){returnab;}看到add(3.14,2.86)时自动用double替换T生成对应函数。我们只写了一套代码编译器帮我们生成了多套代码。上述代码中在进行模板实例化时并没有指明任何类型函数模板在生成模板函数时通过传入的参数类型确定出模板类型这种做法称为隐式实例化。我们在使用函数模板时还可以在函数名之后直接写上模板的类型参数列表指定类型这种用法称为显式实例化。5. 多类型参数的函数模板如果函数需要多种不同类型的参数只需增加模板参数// 两个类型参数T1 和 T2templatetypenameT1,typenameT2voidprint(T1 a,T2 b){coutaa, bbendl;}intmain(){// T1int, T2stringprint(10,hello);// T1double, T2boolprint(3.14,true);return0;}函数模板的重载如果在使用函数模板时传入两个不同类型的参数,会出错此时就需要进行显式实例化。如下指定了类型T为int型虽然s1是short型数据但是会发生类型转换。这个转换没有问题因为int肯定能存放short型数据的所有内容。templateclassTTadd(T t1,T t2){returnt1t2;}voidtest0(){shorts11;inti24;coutadd(s1,s2): add(s1,i2)endl;//errorcoutadd(s1,s2): addint(s1,i2)endl;//ok}但如果是以下这种转换实际上就会损失数据精度。此时的d2会转换成int型。inti14doubled25.3;coutadd(i1,d2): addint(i1,d2)endl;如果一个函数模板无法实例化出合适的模板函数去进行显式实例化也有一些问题的时候可以再给出另一个函数模板//函数模板与函数模板重载//模板参数个数不同,oktemplateclassT//模板一Tadd(T t1,T t2){returnt1t2;}templateclassT1,classT2T1add(T1 t1,T2 t2){returnt1t2;}//模板二double x 9.1;inty10;coutadd(x,y)endl;//会调用模板二生成的模板函数不会损失精度//试一试coutadd(y,x)endl;//返回值是一个int数据如果仍然采用显式实例化,可以传入两个类型参数那么一定会调用模板二生成的模板函数。传入的两个类型参数会作为T1、T2的实例化参数。也可以传入一个类型参数那么这个参数会作为模板参数列表中的第一个类型参数进行实例化。如果仍然需要进行类型转换那么就会使用第一个函数模板进行实例化如果不需要进行类型转换就会使用第二个函数模板进行实例化.intx10;doubley9.2;coutaddint,int(x,y)endl;//模板二coutaddint(x,y)endl;//模板二coutaddint(y,x)endl;//模板一函数模板与函数模板重载的条件1名称相同这是必须的2模板参数列表中的模板参数在函数中所处位置不同 —— 但是强烈不建议进行这样的重载。这样进行重载时要注意隐式实例化可能造成冲突需要显式实例化。如果能够通过类型转换去匹配上两个函数模板的时候即使是显式实例化也很难避免冲突函数模板与普通函数重载普通函数优先于函数模板执行——因为普通函数更快编译器扫描到函数模板的实现时并没有生成函数只有扫描到下面调用add函数的语句时给add传参知道了参数的类型这才生成一个相应类型的模板函数——模板参数推导。所以使用函数模板一定会增加编译的时间。此处就直接调用了普通函数而不去采用函数模板三、类模板通用类的实现函数模板解决了函数的通用化类模板则解决类的通用化比如通用容器、通用工具类。最经典的场景写一个通用的栈Stack类既能存int也能存string还能存自定义对象。1. 类模板语法templatetypenameTclass类名{// 成员变量/方法 都可以使用类型T};2. 实战通用栈类模板#includeiostream#includevectorusingnamespacestd;// 类模板通用栈templatetypenameTclassStack{private:vectorTdata;// 用类型T定义成员变量public:// 入栈voidpush(T val){data.push_back(val);}// 出栈voidpop(){if(!data.empty())data.pop_back();}// 获取栈顶元素Ttop(){returndata.back();}// 判断是否为空boolisEmpty(){returndata.empty();}};intmain(){// 1. 存储int的栈StackintintStack;intStack.push(10);intStack.push(20);coutint栈顶intStack.top()endl;// 2. 存储string的栈StackstringstrStack;strStack.push(C);strStack.push(模板);coutstring栈顶strStack.top()endl;return0;}运行结果int栈顶20 string栈顶模板3. 类模板的使用注意类模板必须显式指定类型不能像函数模板那样自动推导Stackint、Stackstring类模板的成员函数只有在被调用时才会被编译器实例化四、模板的特化特殊类型的定制化处理模板是通用的但某些特殊类型需要单独写逻辑这就是模板特化。举个例子写一个比较大小的函数模板但是对于char*字符串不能直接用比较需要用strcmp。1. 函数模板全特化// 通用模板templatetypenameTTmax(T a,T b){returnab?a:b;}// 针对char*的特化版本templateconstchar*maxconstchar*(constchar*a,constchar*b){returnstrcmp(a,b)0?a:b;}intmain(){// 调用通用模板coutmax(10,20)endl;// 调用特化模板coutmax(apple,banana)endl;return0;}2. 类模板特化类模板也可以针对特定类型重写整个类的逻辑满足定制化需求。模板的参数类型类型参数之前的T/T1/T2等等成为模板参数也称为类型参数类型参数T可以写成任何类型非类型参数需要是整型数据 char/short/int/long/size_t等不能是浮点型float/double不可以定义模板时在模板参数列表中除了类型参数还可以加入非类型参数。如下调用模板时需要传入非类型参数的值templateclassT,intkBaseTmultiply(T x,T y){returnx*y*kBase;}voidtest0(){inti13,i24;coutmultiplyint,10(i1,i2)endl;}可以给非类型参数赋默认值有了默认值后调用模板时就可以不用传入这个非类型参数的值函数模板的模板参数赋默认值与普通函数相似从右到左右边的非类型参数赋了默认值左边的类型参数也可以赋默认值优先级指定的类型 推导出的类型 类型的默认参数模板参数的默认值不管是类型参数还是非类型参数只有在没有足够的信息用于推导时起作用。当存在足够的信息时编译器会按照实际参数的类型去调用不会受到默认值的影响。可变模板参数可变模板参数( variadic templates )是 C11 新增的最强大的特性之一它对参数进行了高度泛化它能表示0到任意个数、任意类型的参数。由于可变模版参数比较抽象使用起来需要一定的技巧所以它也是 C11 中最难理解和掌握的特性之一。可变参数模板和普通模板的语义是一样的只是写法上稍有区别声明可变参数模板时需要在typename 或 class 后面带上省略号 “…” 省略号写在右边代表打包templateclass...Argsvoidfunc(Args...args);类比于C语言中的printf函数的参数个数可能有很多个用…表示参数的个数、类型、顺序可以随意可以写0到任意个参数。我们在定义一个函数时可能有很多个不同类型的参数不适合一一写出所以提供了可变模板参数的方法。定义一个可变模板参数Args里面打包了 T1/T2/T3…这样的一些类型typename… ArgsArgs 代表一组类型int, double, string…args里面打包了函数的参数Args… argsargs 代表一组对应类型的值1, 3.14, “hello”……在左边就是打包的含义利用可变参数模板输出参数包中参数的个数templateclass...Args//Args 模板参数包voiddisplay(Args...args)//args 函数参数包{//输出模板参数包中类型参数个数coutsizeof...(Args) sizeof...(Args)endl;//输出函数参数包中参数的个数coutsizeof...(args) sizeof...(args)endl;}voidtest0(){display();display(1,hello,3.3,true);}intmain(intargc,charconst*argv[]){test0();}/* sizeof...(Args) 0 sizeof...(args) 0 sizeof...(Args) 4 sizeof...(args) 4 */实战写一个支持任意参数的打印函数处理可变参数包最经典、最通用的方法是递归展开递归处理第一个参数剩下的参数包继续递归最后写一个无参递归终止函数完整可运行代码#includeiostreamusingnamespacestd;// 【步骤1】递归终止函数0个参数时调用voidprint(){cout递归结束endl;}voidprint(intx){coutxendl;}// 【步骤2】递归展开函数每次处理第一个参数 剩余参数包templatetypenameT,typename...Argsvoidprint(T first,Args...rest){// 打印当前第一个参数coutfirst ;// 递归展开剩余参数包rest...print(rest...);}intmain(){// 调用可变参数打印print(1,2.5,C可变模板,true);//1 2.5 C可变模板 1 递归结束print(1,hello,3.6,true,100);//1,hello,3.6,true,100 递归结束return0;}只剩下一个int型参数的时候也没有使用函数模板而是通过普通函数结束了递归。关于递归的出口可以使用普通函数或者普通的函数模板但是规范操作是使用普通函数。1尽量避免函数模板之间的重载2普通函数的优先级一定高于函数模板更不容易出错。五、模板的两大核心优势代码复用一套代码适配所有类型大幅减少冗余代码类型安全编译期就会检查类型不会出现类型不匹配的运行时错误高性能模板是编译期展开的没有运行时开销比虚函数、泛型接口更快六、模板的常见坑点模板代码不能分文件编写模板的声明和实现必须放在同一个头文件中因为编译器需要在编译期看到完整代码否则会报链接错误。错误信息晦涩难懂模板代码出错时编译器的报错信息非常长需要重点关注第一行错误和类型相关提示。代码膨胀模板会为每一种类型生成独立代码过度使用会导致可执行文件体积变大合理使用即可避免。七、总结模板到底有什么用C模板是泛型编程的核心它让代码从「针对具体类型」升级为「针对通用逻辑」函数模板通用函数减少函数重载冗余类模板通用类实现通用容器如STL的vector、map都是类模板模板特化特殊类型定制化兼顾通用与灵活在实际开发中STL标准库vector、string、map、sort等全部基于模板实现掌握模板是进阶C开发的必经之路。总结模板是C泛型编程核心分为函数模板和类模板核心语法template typename TT为通用类型参数优势代码复用、类型安全、编译期展开无运行时开销关键模板声明与实现必须放在同一头文件支持特化处理特殊类型如果觉得模板抽象不妨先从简单的函数模板练手再逐步学习类模板你会慢慢体会到它的强大