C++模版
《C++ Templates: The Complete Guide》笔记
为什么使用模版
为了实现泛型
如果不使用泛型,对于一些通用的函数或容器,比如print、vector,我们可能需要逐个实现他们的函数,尽管这些函数的功能十分类似(可能相同),低效复杂而且难以维护
在C++标准库中使用了大量模版
模板的缺点
难以阅读、调试
容易触发编译报错(比如将<<
运算符识别为模板展开)
闭源软件不友好(许多开源软件会提供.h
和.lib
,如果使用模板,头文件中就会暴露实现)
函数模版
模版定义
template <typename T>T getMax (T a, T b) { return a > b ? a : b; } int main () { std::cout << getMax (5 , 6 ) << std::endl; std::cout << getMax (5.1 , 6.1 ) << std::endl; return 0 ; }
这里T
是类型参数 ,我们可以使用任意字符,不过习惯上使用T
处于历史原因,我们也可以使用class
来代替typename
template <class T>T getMax (T a, T b) { return a > b ? a : b; }
模版实例化
在编译阶段,模版会被编译为多个独立的实体,比如我们调用getMax(5, 6)
,编译器会编译出:
int getMax (int a, int b) { return a > b ? a : b; }
像这样用具体类型int
取代类型参数T
的过程叫做实例化
两阶段编译检查
模版分为两步编译:模版定义阶段、模版实例化阶段
在模版定义阶段,不会对类型参数做检查,比如你让两个T
对象进行大小比较(a > b
),编译器不会去质疑T
对象是否支持比较操作符
在模版实例化阶段,模版会再次被检查(尤其是类型参数),如果编译器发现T
对象不支持比较操作符,就会报错
我们发现,当我们在实例化一个模版时,编译器需要看到模版的完整定义,但C++函数采用声明和实现分离的思想,函数在编译阶段只需要声明,这出现了冲突。简单的做法是将模版的实现写在头文件中
类型推断
我们在调用getMax
时,会根据传入的参数类型,自动推断出T
的类型,但这种推断是有限制的
getMax (4 , 5 ); getMax (4.1 , 5.1 ); getMax (4 , 5.1 ); const int a = 1 ;getMax (2 , a); int b = 2 ;int &c = b;getMax (2 , c);
decay
退化(decay)是指数组变指针 ,函数变指针 ,左值变右值 的过程
void foo (int arr[]) {...}int main () { int myArray[5 ] = {1 , 2 , 3 , 4 , 5 }; foo (myArray); }
在上面这段代码,数组myArray
被转化为指向数组第一个元素的指针
void foo () {...}int main () { void (*ptr)() = foo; ptr (); }
在上面这段代码,函数foo
被转化为函数指针
const int a = 1 ;int b = 2 ;int &c = b;getMax (2 , a); getMax (2 , c);
在上面这段代码,int&
和const int&
都被转化为对应的右值
我们可以使用type_traits库来检测decay的合法性
template <typename T, typename U>struct decay_equiv : std::is_same<typename std::decay<T>::type, U>::type {};int main () { std::cout << std::boolalpha << decay_equiv<int &, int >::value << std::endl; std::cout << std::boolalpha << decay_equiv<const int &, int >::value << std::endl; }
std::boolalpha的作用是将bool类型以true/false的形式打印出来,而非1/0
多个模板参数
当模板有多个参数时,我们可以用<>
指定参数类型
在后面的类型,如果可以自动推断出来,可以不写
template <typename RT, typename T1, typename T2>RT getMax (T1 a, T2 b) { return a > b ? a : b; } int main () { std::cout << getMax <int , double , int >(4 , 7.2 ) << std::endl; std::cout << getMax <int >(4 , 7.2 ) << std::endl; }
返回类型推断
极其推荐这样写
上面的返回值类型是手动指定的,很容易出现编写错误,因此我们一般会让编译器来角色返回值类型
template <typename T1, typename T2>auto getMax (T1 a, T2 b) -> decltype (a > b ? a : b) { return a > b ? a : b; } int main () { std::cout << getMax (4 , 7.2 ) << std::endl; std::cout << getMax (8 , 7.2 ) << std::endl; }
我们发现这样写的返回类型由?:
运算符的执行结果决定,7.2大于4,于是返回类型为double
,8大于7.2,于是返回类型为int
进一步的,若传入的数据是引用类型,可以使用类型萃取,不过一般情况下,上面这种写法就够了
template <typename T1, typename T2>auto getMax2 (T1 a, T2 b) -> typename std::decay< decltype (a > b ? a : b) >::type { return a > b ? a : b; }
公共类型
Common Type
C++11提供了一种更一般的类型,用于得到两个模板参数的公共类型
template <typename T1, typename T2>std::common_type_t <T1, T2> getMax3 (T1 a, T2 b) { return a > b ? a : b; }
默认参数类型
我们可以给参数指定默认值,但要同时给T
一个对应默认参数
template <typename T = double >void foo (T a = 3.14 ) { std::cout << a << std::endl; } int main () { foo (15 ); foo (); }
重载
函数模板可以与同名的普通函数共存,且优先调用普通函数
template <typename T>void foo (T a) { std::cout << a << std::endl; } void foo (int a) { std::cout << "int: " << a << std::endl; } int main () { foo (1 ); foo (3.14 ); }
函数模板也可以和其他同名但参数类型数量不同的函数模板共存,但必须保证在调用模板时,有且仅有一个模板能匹配
template <typename T1, typename T2> auto max (T1 a, T2 b) { return b < a ? a : b; }template <typename RT, typename T1, typename T2> RT max (T1 a, T2 b) { return b < a ? a : b; }
注意事项
使用值传递
一般而言,函数参数中,简单类型值传递,复杂的类型引用传递。但在模板编程中,我们更倾向于使用值传递
值传递的优点:
语法简单
编译器能更好地优化
移动一般比拷贝成本更低
某些情况没有移动或拷贝
模板既可以适用于复杂类型,也可以适用于简单类型,盲目使用引用会影响简单类型的使用
调用者可以主动使用std::ref()
和std::cref()
string literal和raw array使用引用传递会出现问题
类模板
类也可以使用模板,STL中的容器就是这样实现的
类模版中的模版成员函数,只有在被调用时才会实例化
类模版的模版参数,要能支持模版函数中使用的各种操作和运算符
template <typename T>class Stack {private : std::vector<T> elems; public : void push (T const & elem) ; void pop () ; T const & top () const ; bool empty () const { return elems.empty (); } }; template <typename T>void Stack<T>::push (const T &elem) { elems.push_back (elem); } template <typename T>void Stack<T>::pop () { elems.pop_back (); } template <typename T>T const & Stack<T>::top () const { assert (!elems.empty ()); return elems.back (); }
由于历史原因,C++11以前的模版,两个相邻尖括号间要有空格,如Stack<Stack<int> >
,在C++11之后就不再需要了
静态成员
template <typename T>class Foo {public : static int count; }; template <typename T>int Foo<T>::count = 0 ;int main () { Foo<int >::count = 10 ; Foo<double >::count = 20 ; std::cout << Foo<int >::count << std::endl; std::cout << Foo<double >::count << std::endl; }
特例化
对于特定的类型,我们可以进行特例化,可以做针对性的优化
template <>class Stack <std::string> {... };
特例化也可以部分特例化
template <typename T>class Stack <T*> {... };
可以多模版参数特例化,可以给予默认模版参数
类型别名
类型别名只是为已经存在的类型定义一个别名,并没有创建新类型,可以用typedef
或using
实现
typedef-name:
typedef Stack<int > IntStack;
alias declaration
using IntStack2 = Stack<int >;
alias declaration也可以被模版化,被称为alias templates
template <typename T>using MyStack = Stack<T>;
非类型模版参数
所谓的非类型模版参数,就是不用typename T作为模版参数
下面是使用array
实现Stack
的代码,用户可以手动指定栈容量,我们使用了std::size_t
作为非类型模版参数
template <typename T, std::size_t Maxsize>class Stack {private : std::array<T, Maxsize> elems; std::size_t elemCount; public : Stack (); void push (T const & elem) ; void pop () ; T const & top () const ; bool empty () const { return elemCount == 0 ; } std::size_t size () const { return elemCount; } }; template <typename T, std::size_t Maxsize>Stack<T, Maxsize>::Stack (): elemCount (0 ) {} template <typename T, std::size_t Maxsize>void Stack<T, Maxsize>::push (const T &elem) { assert (elemCount < Maxsize); elems[elemCount] = elem; ++elemCount; } template <typename T, std::size_t Maxsize>void Stack<T, Maxsize>::pop () { assert (!elems.empty ()); --elemCount; } template <typename T, std::size_t Maxsize>T const & Stack<T, Maxsize>::top () const { assert (!elems.empty ()); return elems[elemCount-1 ]; } int main () { Stack<int , 10 > st10; st10.push (15 ); std::cout << st10.top () << std::endl; }
值得注意的是,Stack<int, 10>
和Stack<int, 20>
是两种不同的类型,由于没有定义隐式或显示的转换规则,我们不能使用一个取代另一个,也不能把一个赋值给另一个
有效类型
非类型模版参数只能是整形、枚举、指向对象/函数/成员的指针、指向对象/函数的左值引用、nullptr
template <double VAT> double process (double v) { return v * VAT; }
若表达式中使用了>
,要用()
包裹起来
template <int I, bool B>class C ;int main () { C<42 , sizeof (int > 4 )> c; C<42 , (sizeof (int ) > 4 )> c; }
auto
template <typename T, auto Maxsize>class Stack {private : std::array<T, Maxsize> elems; std::size_t elemCount; public : using size_type = decltype (Maxsize); Stack (); void push (T const & elem) ; void pop () ; T const & top () const ; bool empty () const { return elemCount == 0 ; } size_type size () const { return elemCount; } };
变参模版
就是接受一组数量可变的参数
template <typename T>void print (T arg) { std::cout << arg << " " ; } template <typename T, typename ... Types>void print (T first, Types... args) { print (first); print (args...); } template <typename C, typename ... Index>void printElems (C const & coll, Index... id) { print (coll[id]...); } int main () { print ("hello" , "world" , 1314 ); int a[] = {1 , 2 , 3 , 4 , 5 , 6 , 7 }; printElems (a, 2 , 3 ); }
折叠表达式
几乎所有二元运算符都可以用于折叠表达式
template <typename ...T>auto getSum (T...s) { return (...+s); } int main () { std::cout << getSum (1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ) << std::endl; }
基础技巧
typename
template <typename ...T>void foo (T const & coll) { T::const_interator pos; typename T::const_interator pos; }
零初始化
若一个类型没有默认构造函数(比如基础类型和指针类型),被初始化前其值是未定义 的,我们可以使用值初始化
template <typename T>void foo1 () { T x {}; std::cout << x << std::endl; } template <typename T>void foo2 () { T x; std::cout << x << std::endl; } int main () { foo2 <int >(); foo2 <int >(); foo1 <int >(); foo2 <int >(); }
我们可以在默认构造函数中使用零初始化
class Foo {private : T x; public : Foo () : x{} { } void print () { std::cout << x; } }; int main () { Foo<int > x; x.print (); }
如果是非静态成员,也可以这样写
template <typename T>class Foo {private : T x {}; public : void print () { std::cout << x; } };
this
对于类模版,若基类也依赖于模版参数,那么子类在调用基类的成员时要使用this->
或Base<T>::
修饰
template <typename T>class Base {public : void print () { std::cout << "Hello world" ; } }; template <typename T>class Foo : Base<T> {public : void foo () { this ->print (); } }; int main () { Foo<int > f; f.foo (); }
成员模版
类的成员也可以是模版,而且模版参数可以不同
template <typename T>class Stack {private : std::deque<T> elems; public : void push (T const &) ; void pop () ; T const & top () const ; bool empty () const { return elems.empty (); } template <typename T2> Stack& operator = (Stack<T2> const &); template <typename > friend class Stack ; }; template <typename T>template <typename T2>Stack<T>& Stack<T>::operator =(const Stack<T2> &op2) { elems.clear (); elems.insert (elems.begin (), op2.elems.begin (), op2.elems.end ()); return *this ; } int main () { Stack<float > floatStack; for (int i = 0 ; i < 5 ; i++){ float n = 3.14 * i; floatStack.push (n); std::cout << n << " " ; } std::cout << std::endl; Stack<int > intStack; intStack = floatStack; while (!intStack.empty ()){ std::cout << intStack.top () << " " ; intStack.pop (); } }
lambda
lambda表达式本质上是成员模版的简化
[](auto x, auto y){ return x + y; }
变量模版
变量也可以使用模版,必须指定类型
template <typename T = long double >constexpr T pi {3.1415926535897932385 };int main () { std::cout << pi<float > << std::endl; std::cout << pi<double > << std::endl; std::cout << pi<> << std::endl; }
C++17中类型萃取就是使用了变量模版
namespace std{ template <typename T> constexpr bool is_const_v = is_const<T>::value; }
模版参数模版
模版参数也可以是一个类模版
template <typename T, template <typename Elem> class Cont = std::deque>class Stack{private : Cont<T> elems; public : ... };
移动语义
移动的本质是所有权的转移,这里不过多赘述
移动(move):将原对象拷贝或赋值给目标对象时,若原对象马上要被销毁,可以将原对象对内部资源和状态的所有权 直接转移给目标对象,避免了非必要的拷贝和临时对象
禁用函数模版
C++11提供了辅助模版std::enable_if<>
,可以在编译期间忽略掉一些函数模版
注意看,foo()
前面的返回值类型是由std::enable_if
决定的,
template <typename T>typename std::enable_if<(sizeof (T) > 4 )>::type foo () { std::cout << sizeof (T) << std::endl; } int main () { foo <int >(); foo <double >(); foo <long long >(); }
若(sizeof (T) > 4)
为false,根据模版中SFINAE(substitute failure is not an error)规则,替换失败不是错误,而是会将这个函数模版忽略掉,于是foo<int>()
会报找不到函数的错
若(sizeof (T) > 4)
为true,std::enable_if<>
会被拓展为void
,如果你给了第二个参数T
,std::enable_if<>
会被拓展为T
类型
template <typename T>std::enable_if_t <(sizeof (T) > 4 ), T> foo (){ std::cout << sizeof (T) << std::endl; }
更明智的写法是
template <typename T>using EnableIfSizeGreater4 = std::enable_if_t <(sizeof (T) > 4 )>;template <typename T, typename = EnableIfSizeGreater4<T>>void foo (){ std::cout << sizeof (T) << std::endl; }
禁用构造函数模版
由于转移语义的存在,构造函数的参数类型很可能是错误的,比如传入了一个右值引用
下面是一个例子
template <typename T>using EnableIfString = std::enable_if_t <std::is_convertible_v<T, std::string>>;class Person {private : std::string name; public : template <typename STR, typename = EnableIfString<STR>> explicit Person (STR&& n) : name (std::forward<STR>(n)) { std::cout << "模版构造函数 " << name << std::endl; } Person (Person const & p) : name (p.name) { std::cout << "拷贝构造函数 " << name << std::endl; } Person (Person&& p) : name (std::move (p.name)) { std::cout << "移动构造函数 " << name << std::endl; } }; int main () { std::string s = "sname" ; Person p1 (s) ; Person p2 ("temp" ) ; Person p3 (p1) ; Person p4 (std::move(p1)) ; }
我们没法禁用拷贝和移动构造函数,因为我们用成员函数来替代这些函数时,这些函数还会生成默认构造函数
concept
enable_if
的语法非常丑,而且还额外使用了一个模版参数,使得代码不易读懂
我们需要的是一个能对函数施加限制的语言特性,当限制不满足时函数会被忽略掉,于是C++决定引入concept
模板元编程
模板在编译器实例化,在实例化时可以进行简单的计算,这就是模板元编程
下面是用模板元判断一个数是不是质数
template <unsigned int N, unsigned int Divisor>struct IsPrime { static constexpr bool value = (N % Divisor != 0 ) && IsPrime<N, Divisor - 1 >::value; }; template <unsigned int N>struct IsPrime <N, 2 > { static constexpr bool value = (N % 2 != 0 ); }; template <unsigned int N>constexpr bool isPrime = IsPrime<N, N / 2 >::value;int main () { constexpr unsigned int number = 17 ; if (isPrime<number>) { std::cout << number << " is a prime number." << std::endl; } else { std::cout << number << " is not a prime number." << std::endl; } }
decltype
选择合适的模板特化,也叫SFINAE
template <typename T, typename U>auto add (T t, U u) -> decltype (t + u) { return t + u; } int main () { std::cout << add (1 , 2 ) << std::endl; std::cout << add (1 , 1.5 ) << std::endl; std::cout << add (1 , 1.5f ) << std::endl; return 0 ; }
if constexpr
编译器if
template <typename T>float addIfFloat (T t) { if constexpr (std::is_integral_v<T>) { return t; } return t + 1.0f ; } int main () { std::cout << addIfFloat (1 ) << std::endl; std::cout << addIfFloat (1.5f ) << std::endl; }
类型萃取
感觉不如C#的反射
用于编译期获得模板参数的性质,下面是判断一个类中是否有成员函数Execute()
的示例
template <typename T>struct hasFunction_Execute {private : template <typename U> static auto check (U* ptr) -> decltype (std::declval<U>().Execute(), std::true_type{}) ; template <typename U> static std::false_type check (...) ; public : static constexpr bool value = decltype (check <T>(nullptr ))::value; };
struct Check { void Execute () { std::cout << "Check::someFunction() called." << std::endl; } }; struct NoCheck { }; int main () { if (hasFunction_Execute<Check>::value) { Check obj; obj.Execute (); } if (hasFunction_Execute<NoCheck>::value) { } return 0 ; }
注意这里使用了std::declval
,可以用作引用占位符