类模板和模板类
类模板
类模板:允许用户为类定义一种模式,使得类中的某些数据成员、默认成员函数的参数、某些成员函数的返回值,能够取任意类型(包括系统预定义和用户自定义)。如果一个类中数据成员的数据类型不能确定,或者是某个成员函数的参数或返回值的类型不能确定,就必须将此类声明为模板,它的存在不是代表一个具体的、实际的类,而是代表一类 类
1 2 3 4 5 6 7 8 9 10 11 12 13
| template <class T> class foo { private: T n; const T i; static T cnt; public: Test(): i(0) {} Test(T k); ~Test(){} void print(); T operator+(T x); };
|
在类定义体外定义成员函数时,若此成员函数中有模板参数存在,则除了需要和一般类的体外定义成员函数一样的定义外,还需在函数体内进行模板声明
1 2 3 4
| template <class T> void Test<T>::print() { ... }
|
在类定义体外初始化 const 成员和 static 成员变量的做法和普通类体外初始化 const 成员和 static 成员变量的做法基本上时一样的,唯一的区别是需再对模板进行声明
1 2
| template<class T> int Test<T>::cnt = 0;
|
模板类
模板类其实就是 类模板实例化后的一个产物。我们把类模板比作一个做饼干的模子,而模板类就是用这个模子做出来的饼干
函数模板和模板函数
函数模板可以用来创建一个通用的函数,以支持多种不同的形参,避免重载函数的函数体重复设计。它的最大特点是把函数使用的数据类型作为参数。
模板函数的生成就是将函数模板的类型形参实例化的过程。
C++ template 的图灵完备
图灵完备:在可计算理论中,当一组数据操作的规则(一组指令集、编程语言,或者细胞自动机)满足任意数据按照一定的顺序可以计算出结果,被称为 图灵完备。
C++模板是图灵完全的,使用C++模板,可以在编译期间模拟一个完整的图灵机,也就是说,可以完成任何的计算任务
模板元编程(编译期计算)
1 2 3 4 5 6 7 8 9 10 11 12 13
| template <int n> struct factorial { static const int value = n * factorial<n-1>::value; };
template <> struct factorial<0> { static const int value = 1; };
int main() { std::cout << factorial<10>::value << std::endl; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| template <bool cond, typename Then, typename Else> struct If;
template <typename Then, typename Else> struct If<true, Then, Else> { typedef Then type; };
template <typename Then, typename Else> struct If<false, Then, Else> { typedef Else type; };
int main() { std::cout << typeid(If<true, int, double>::type).name() << std::endl; }
|
使用模板来做一些高级的运算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| template <bool condition, typename Body> struct WhileLoop;
template <typename Body> struct WhileLoop<true, Body> { typedef typename WhileLoop<Body::cond_value, typename Body::next_type>::type type; };
template <typename Body> struct WhileLoop<false, Body> { typedef typename Body::res_type type; };
template <typename Body> struct While { typedef typename WhileLoop<Body::cond_value, Body>::type type; };
template <class T, T v> struct integral_constant { static const T value = v; typedef T value_type; typedef integral_constant type; };
template <int result, int n> struct SumLoop { static const bool cond_value = n != 0; static const int res_value = result; typedef integral_constant<int, res_value> res_type; typedef SumLoop<result + n, n - 1> next_type; };
template <int n> struct Sum { typedef SumLoop<0, n> type; };
int main() { std::cout << While<Sum<10>::type>::type::value << std::endl; }
|
注意:使用 :: 取一个成员类型、并且 :: 左边有模板参数的话,得额外加上 typename 关键字来标明结果是一个类型。
编译期运行推导
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <type_traits>
typedef std::integral_constant<bool, true> true_type; typedef std::integral_constant<bool, false> false_type;
template <typename T> class SomeContainer { public: static void destroy(T* ptr) { _destroy(ptr, std::is_trivially_destructible<T>()); }
private: static void _destroy(T* ptr, true_type) {} static void _destroy(T* ptr, false_type) { ptr->~T(); } };
|
很多容器类会有一个 destroy 函数,通过指针来析构某个对象。为了确保最大程度的优化,常用的技巧就是用 std::is_trivially_destructible
模板来判断类是否是可平凡析构的,也就是说,不调用析构函数,不会造成任何资源泄露问题。模板返回的结果还是一个类,要么是 true_type,要么是 false_type。如果要得到布尔值的话,当然使用 is_trivially_destructible::value 就可以,但此处不需要。我们需要的是,使用 () 调用该类型的构造函数,让编译器根据数值类型来选择合适的重载。这样,在优化编译的情况下,编译器可以把不需要的析构操作彻底全部删除。
类型转换用途
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <iostream> #include <typeinfo>
template <class T> struct remove_const { typedef T type; };
template <class T> struct remove_const<const T> { typedef T type; };
int main() { std::cout << typeid(remove_const<const char*>::type).name() << std::endl; }
|
利用模板的特化,针对 const 类型去掉相应的属性。注意:const char* 应用于remove_const 的话,结果还是 const char*
,原因是,const char*
是指向 const char 的指针,而不是指向 char 的const 指针。这个const 修饰的是指针指向的内容,而不是指针本身。如果对 char* const
应用 remove_const
的话,还是可以得到 char*
的
函数模板的重载决议
模板之 SFINAE(替换失败非错(substation failure is not an error))
当一个函数名称和某个函数模板名称匹配时,重载决议过程大致如下:
- 根据名称找出所有适用的函数和函数模板
- 对于适用的函数模板,要根据实际情况对模板形参进行替换;替换过程中如果发生错误,这个模板会被丢弃
- 在上面两步生成的可行函数集合中,编译器会寻找一个最佳匹配,产生对该函数的调用
- 如果没有找到最佳匹配,或者找到多个匹配程度相当的函数,则编译器需要报错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <iostream>
struct Test { typedef int foo; };
template <typename T> void f(typename T::foo) { puts("1"); }
template <typename T> void f(T) { puts("2"); }
int main() { f<Test>(10); f<int>(10); }
1 2
|
分析:
1 2 3 4 5 6 7 8
| 对于 f<Test>(10); 的情况: 1. 我们有两个模板符合名字 f 2. 替换结果为 f(Test::foo) 和 f(Test) 3. 使用参数10去匹配,只有前者参数可以匹配,因而第一个模板被选择 对于 f<int>(10); 的情况: 1. 还是两个模板符合名字 f 2. 替换结果为 f(int::foo) 和 f(int); 显然前者不是个合法的类型,被抛弃 3. 使用参数 10 去匹配 f(int),没有问题,那就使用这个模板实例了
|
SFINAE 设计的最初用法:如果模板实例化中发生了失败,没有理由编译就此出错终止,因为还是可能有其他可用的函数重载的。这儿失败仅指函数模板的原型声明,即参数和返回值。函数体内的失败不考虑在内。如果重载决议选择了某个函数模板,而函数体在实例化的过程中出错,那我们仍然会得到一个编译错误。
可变模板实现编译期递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| template <typename T> constexpr auto sum(T x) { return x; }
template <typename T1, typename T2, typename... Targ> constexpr auto sum(T1 x, T2 y, Targ... targ) { return sum(x+y, targ...); }
int main() { auto result = sum(1,2,3,4,5,6,7,8,9,10); std::cout << result << std::endl; }
|
f189e2c8a73a1955b6dc08495a6b37c2b2b17d57