详解C++ 模板编程
类型模板
类型模板包括函数模板和类模板,基本上是c++开发人员接触模板编程的起点。
下面代码演示了函数模板和类模板的使用方法:
// 函数模板 template<typename t> t add(const t& a, const t& b) { return a + b; } // 类模板 template<typename t> class point { private: t x[3]; ... };
类型模板以template
开始声明,尖括号内的typename
关键字可用class
替代。类型模板中typename
和class
具有相同含义,均表示参数类型。实践中typename
语义更广泛,表示其后续的参数t
是一个类型,不限定于类,建议使用。类型参数t
可换成其他任意有意义的合法变量。
c++14新增变量模板:
// 变量模板 template<tyepename t> constexpr t pi = t(3.1415926535897932385l);
尖括号之于模板犹如小括号之于函数:函数通过小括号()定义和调用,模板使用尖括号<>定义(需template关键字声明)和实例化。上面演示了类型模板定义,下面代码介绍模板实例化:
int a = 1, b = 2; // 实例化函数模板 std::cout << "add result:" << add<int>(a, b) << std::endl; // 实例化类模板 auto p = point<int>(); double radius = .5; // 实例化变量模板 auto area = pi<double> * radius * radius;
同函数一样,模板可以有默认值:
// 默认类型为int template<typename t=int> t add(const t& a, const t& b) { return a + b; } // 默认类型为double template<typename t=double> class point { private: t x[3]; ... };
与函数不同,对于函数模板,如果能从参数推断出模板类型,则可略去尖括号模板实例化参数:
int a = 1, b = 2; // 合法调用,编译器能根据a b推断出参数类型 std::cout << "add result:" << add(a, b) << std::endl; // 等同于 std::cout << "add result:" << add<int>(a, b) << std::endl;
然而对于类模板,即使有默认参数,也不能省略尖括号(但是可以省去参数):
template<typename t=double> struct point { t x[3]; }; // 合法声明 auto p = point<double>(); // 合法声明,类型使用默认的double auto p2 = point<>(); // 非法声明,缺少模板调用标志尖括号 auto p3 = point();
类型参数模板在实际中使用最多,stl库中vector、map等容器、algorithm中的许多算法都用到了模板。
非类型参数模板
另一类常用模板是非参数模板,用来替代某个具体的值。例如:
// n维空间向量 template<int n> struct vector { double x[n]; }; // 实例化 auto v = vector<100>(); ...其他操作
需要注意的是,非类型参数模板能使用的类型十分有限,只有(signed/unsigned)整数、char和枚举这几种类型可用(参考switch
语法)。
同类型模板一样,非类型参数模板也可以有默认值,但应用到类模板实例化也不能省略尖括号。
类型模板和非类型参数模板可以结合一起用:
template<typename t, int n> struct point { t x[n]; };
类型模板解决了类型问题,非类型参数模板解决了值的问题,实际中应用也十分广泛。作为递归的经典场景,斐波那契数列可以用非类型模板解决:
template<int n> struct fib { static constexpr int value = fib<n-1>::value + fib<n-2>::value; }; // 模板特化 template<> struct fib<1> { static constexpr int value = 1; }; // 模板特化 template<> struct fib<0> { static constexpr int value = 0; }; // 调用 std::cout << "fib(10): " << fib<10>::value << std::endl;
这个例子出现了”模板特化”,接下来介绍。
模板特化/偏特化
定义模板后,希望在特定条件下使用单独的模板,这便是模板特化。上文中斐波那契数列定义的template<int n> struct fib是母模板,接下来又定义了0和1两个特化模板(子模板),指示编译器遇到fib<0>和fib<1>的情况,使用这两组单独定义。需要注意的是特化模板的template参数为空,具体模板参数放到了模板名称处,类似于模板实例化。
对多个模板参数的情形,如果只特化某个模板参数,便是偏特化。例如:
// 泛型模板定义 template<typename t1, typename t2> struct add; // 特化模板 template<> struct add<int, int> {...}; // 偏特化模板 template<typename t> struct add<t, long> {....};
模板特化/偏特化类似于函数重载,能针对特殊情况进行特别处理。
模板匹配与sfinae
模板特化使得同一个模板名称有了多个定义,代码具体调用时会遇到模板匹配问题。理解模板匹配机制的关键便是sfinae,这也是进阶模板编程的必备知识点。
sfinae是substitution failure is not an error的缩写,翻译过来便是:匹配(替换)失败不是错误。
怎么理解这句话呢?
对于上面的斐波那契数列数列代码,编译器遇到fib<10>::value的代码,(可能)先会尝试匹配fib<0>,发现匹配不上,这是一个substitution failure,但不是error,所以编译器继续尝试其他可能性。接着匹配fib<1>,同样发现匹配不上,忽略这个substitution failure继续尝试fib<n>,ok,这一次没问题,编译成功。
如果是fib<-1>::value,编译器达到最大递归深度也找不到一个合适的匹配模板,这是一个error,因此编译失败。
备注:理解上面的话需要对编译过程稍加了解,编译过程会输出许多信息,编译器一般只有遇到error才会终止编译,比较常见的warning则不会。模板匹配中的substitution可能连warning都算不上,不会影响编译器继续尝试匹配
理解sfinae是看懂稍微深奥点模板代码的基本功,重点便是:不怕你模板多,就怕找不到合适的模板。
两阶段编译
有了模板(元)编程,c++源码编译可以分为前期和后期,构成两阶段编译。前期是模板的天下,编译器扫描模板实例化语句,生成运算结果和具体代码;后期编译器介入,再编译生成机器码。
模板代码运行在编译期,因此有如下特点:
- 没有实例化的模板代码,即使有语法错误,编译器也不会检查和报错。对按代码行数考核kpi的c++码农,这绝对是福音,新增template代码十万行,瞎编乱写都可以,只要不实例化,永远能编译通过,编译后的文件大小(一般)不变,也不影响现有代码运行;
- 对于常量,编译前期直接计算,没有运行时开销。上文中的斐波那契数列值在编译期便已经计算出来了;
- 无法运行期动态调用代码。例如下面的要求做不到:
template<int n> struct point {double x[n];}; // 根据输入动态生成类,无法实现和编译成功 int n; std::cin >> n; auto p = new point<n>();
- 模板和多态/虚函数(理念)冲突。多态/虚函数的关键是运行期动态调用代码,而模板在编译期确定,因此两者理念上是冲突的。所以,如果你想一个成员函数既是模板函数,又是虚函数,怎么做实现预期?
c/c++编译有个预处理过程,只是做简单字符串替换,没有具体运算,与模板生成代码不同
在编译前期,除了模板代码被解释执行,其他代码信息都在,因此模板代码拥有类似反射/自省的能力,这也是c++元编程功能强大的原因之一。
c++11中的变化
c++11带来了许多新特性和重大更新,可以认为c++11是一门新的语言。就模板来说,主要更新点如下:
1. 可以使用static constexpr int代替早期模板代码中的enum。网上许多斐波那契数列代码都是基于早期c++,一律使用enum方式定义字段;
2. 可以使用using
代替typedef
。这是using语句能力的重大更新,早期我们定义类型或者别名都需要typedef
,自c++11开始,简单使用using
就可以达到相同效果。
3. c++14引入了变量模板,上文已介绍。
模板优缺点
上文根据自己理解和实践简要介绍了c++模板编程的相关概念,本节总结一下c++模板的优缺点:
c++模板编程优点:
- 减少代码输入,提高代码重用和编程效率;
- 支持鸭子类型(duck typing)的特性,使用便利,功能强大;
- 某些情况下能减少运行期开销;
- 能实现元编程,c++高手必备之路;
c++模板编程缺点:
- 语法看起来是hack黑科技,代码可读性差,编写繁琐;
- 模板代码调试困难,生成的错误信息也晦涩难懂。你可以还记得刚开始使用stl模板的map等数据类型报错的恐怖提示?
- 编译时间增加。
感谢阅读,欢迎指正!
以上就是详解c++ 模板编程的详细内容,更多关于c++ 模板编程的资料请关注其它相关文章!
上一篇: C++11/14学习(六)面向对象增强