C++ 模板编程
1. 基础
1.1 模板函数
1 2 3 4 5 6 7 8 9 10 template <typename T>void Func (T value) ;template <typename T>T Func (T a, T b) ;template <typename T1, typename T2>T2 Func (T1 a, T1 b) ;
1.2 模板类/结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 template <typename T>class Test1 {public : T val; void Func (T val) ; };template <typename T1, typename T2>class Test2 {public : T1 val; void Func (T2 val) ; T2 Func (T1 v1, T2 v2) ; };
2. 特化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 template <typename T1, typename T2, typename T3>class Test { };template <typename T2>class Test <int , T2, float > { };template <>class Test <int , bool , float > { };
需要注意的是,偏特化的概念不适用于模板函数。但是模板函数可以通过函数重载的方式来实现偏特化全特化的效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 template <typename T1, typename T2, typename T3>void Func (T1 a, T2, b, T3 c) ;template <typename T2>void Func (int a, T2 b, float c) ;void Func (int a, bool b, float c) ;template <>void Func <float , int , bool >(float a, int b, bool c);
3. 可变模板参数
3.1 老方法
在写函数的时候,有时会遇到需要传入不确定数量参数的情况。在 C 的时候可以使用 stdarg.h
下的系列宏来实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <cstdarg> int Sum (int cnt, ...) { va_list args; va_start (args, cnt); int sum = 0 ; for (int i = 0 ; i < cnt; ++i) { sum += va_arg (args, int ); } va_end (args); return sum; }
此处 cnt
表示传入参数的数量。
相关资料可以参考 https://en.cppreference.com/w/cpp/utility/variadic
3.2 新方法:参数包
在 C11
之后,引入了 参数包(Parameter pack) 这一个特性。可以配合 C++ 的模板(template)来实现各种可变参数的功能。
1 2 template <typename ... Args>int Sum (Args... args) ;
这里的 Args... args
就是一个参数包,要访问参数包中的参数,有两种方法。
递归的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 template <typename T>void print (T val) { std::cout << val << std::endl; }template <typename T, typename ... Args>void print (T start, Args... args) { std::cout << start << std::endl; print (args...); }
包展开方法(Pack Expansion)
包展开的详细信息可以参考 https://en.cppreference.com/w/cpp/language/parameter_pack
1 2 3 4 5 6 template <typename ... Args>void print (Args... args) { ((std::cout << args << " " ), ...); std::cout << std::endl; }
这里的 (std::cout << args << " ")
即是包展开的一个单元。
可以在 C++ Insights 中看到这段代码会实际展开成如下所示(假设传入三个参数):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 template <typename ... Args>void print (Args... args) { (((std::cout << args) << " " ) , ...); std::cout.operator <<(std::endl); }#ifdef INSIGHTS_USE_TEMPLATE template <>void print <int , int , int >(int __args0, int __args1, int __args2) { (std::operator <<(std::cout.operator <<(__args0), " " )), (std::operator <<(std::cout.operator <<(__args1), " " )), (std::operator <<(std::cout.operator <<(__args2), " " )); std::cout.operator <<(std::endl); }#endif
如果仅一行代码还是没法对参数完成操作,可以考虑使用函数来作为包展开的单元:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <iostream> template <typename ... Args>void print (int & sum, Args... args) { (Add (sum, args), ...); }template <typename T>void Add (int & sum, T val) { sum += val; std::cout << val << std::endl; }int main () { int sum = 0 ; print (sum, 3 , 2 , 3 , 4 ); std::cout << sum << std::endl; return 0 ; }
当然匿名函数也是可以的。
1 2 3 4 5 6 7 8 template <typename ... Args>void print (int & sum, Args... args) { ([&]()->void { sum += args; std::cout << args << 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 template <typename ... Args>float Sum (Args... args) { float sum = 0 ; ((sum += args), ...); return sum; }template <int ... args>int Sum () { int sum = 0 ; ((sum += args), ...); return sum; }int main () { std::cout << Sum (1 , 1.2f , 3.314f , 7.5f ) << std::endl; std::cout << Sum <1 , 2 , 3 , 4 , 5 >() << std::endl; return 0 ; }
4. 为模板实例化添加限制
在实际开发中,有时会遇到不希望模板被某种类型实例化的情况。从 C11 开始,引入了 std::enable_if
来处理这个问题。它会在满足条件的时候使用一个指定的类型来定义变量或者函数(本文后面的例子都是函数),否则会产生报错(当然可以捕获这个异常并输出报错日志,来保证程序可以继续运行)。
其中的布尔值条件可以通过 std::is_xxxx
来进行设置。一下为几种常见的:
std::is_same
:判断两个类型是否一样;
std::is_class
:是否是某个类;
std::is_base_of
:是否是某个类的子类,这里可以交换顺序判断是否是某个类的父类;
std::is_array
:是否是数组;
用法
1 2 3 4 5 6 template <typename T>typename std::enable_if<true , int >::type AddTwo (T val) { return val + 2 ; }
这里有几个需要注意的关键点:
enable_if<true, int>::type
:这里第一个参数是 true
的时候,后面的 type
才有效,type
对应的类型是第二个参数的类型,即 int
;
此处如果函数返回值是 void,则可以省略第二个参数;
要用 typename
修饰 enable_if<true, int>::type
,这样才能表示一个完整的类型,不然会报错。
接下来用 is_same
来替换第一个参数,从而让改模板不能生成 std::string
所对应的实例。
1 2 3 4 5 6 template <typename T>typename std::enable_if<!std::is_same<std::string, T>::value, int >::type AddTwo (T val) { return val + 2 ; }
5. 来点复杂的应用
5.1 只接收 int 和 float 类型
上文实现的 print()
函数,我们想要限制传入的参数必须是 int
或者 float
类型,同时让它返回 sum
,可以写成代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 template <typename T>struct is_float : std::is_same<T, float > {};template <typename T>struct is_int : std::is_same<T, int > {};template <typename ... Args>typename std::enable_if<((is_float<Args>::value || is_int<Args>::value ) && ...), float >::type print (Args... args) { int sum = 0 ; ([&]()->void { sum += args; std::cout << args << std::endl; }(), ...); return sum; }
5.2 禁止使用模板类作为模板参数的模板函数
1 2 3 4 5 6 7 8 9 10 11 12 template <typename T>struct is_template_class : std::false_type {};template <template <typename ...> class Tmp , typename ... T>struct is_template_class <Tmp<T...>> : std::true_type {};template <typename T>typename std::enable_if<!is_template_class<T>::value>::type Func (T value) { std::cout << "Not a template class" << std::endl; }
5.3 禁止使用 vector 容器作为模板参数的模板函数
1 2 3 4 5 6 7 8 9 10 11 12 template <typename T>struct is_vector : std::false_type {};template <template <typename ...> class Tmp , typename ... T>struct is_vector <Tmp<T...>> : std::is_same<Tmp<T...>, std::vector<T...>> {};template <typename T>typename std::enable_if<!is_vector<T>::value>::type Func (T value) { std::cout << "Not Vector" << std::endl; }