文章目录
- 模板
- 模板的声明与定义
- 函数模板
- 非类型模板参数
- 类模板
- 类的成员函数定义
- 构造函数的定义
- 类的静态成员的定义
- 类模板的实例化
- 使用模板类型中的类型成员
- 默认模板参数
- 指定显示模板实参(函数模板显示实参)
- 引用折叠和右值引用参数
- 可变参数模板
- 对参数包的扩展
- 对参数包的转发
- 可变参数模板的示例
- 模板参数传入可调用对象
虽然用了多年的C++,但是C++泛型编程在开发工作中接触的很少,就连需要使用模板的场景都很少。因为用的少,模板相关的知识就是看了忘,忘了看。
但是在阅读一些开源的C++项目,模板还是经常遇到的,为了理解代码,模板的基础知识还是得了然于心。这篇文章是记录了C++模板基础知识 ,内容来自 C++ Primer,做了归纳方便以后查看。
模板
模板是泛型编程的基础。分别有模板函数和模板类,声明一个模板函数或模板类与定义一个普通函数和类一样,只是并不指定明确的类型了。
模板包含了模板实例的步骤,对不同类型相同模板的函数或类,编译器在编译阶段生成对应的代码。不同类型的模板实例化是不同的类型。
模板的声明与定义
为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常即包括声明也包括定义。
函数模板和类模板成员函数的定义通常放在头文件中。像常规的头文件和源文件分离编译的情况,在模板情况下行不通。
如下模板类Blob
,在blob_test.h
文件中
#include <string>
template <typename T,typename U>
class Blob {
public:Blob(T a,U b);
public:T getA();U getB();
private:T _a;U _b;
};
如果将Blob
类中的构造函数及getA()
和getB()
函数,定义在文件blob_test.cpp
中,如下:
#include "blob_test.h"
template <typename T,typename U>
Blob<T,U>::Blob(T v,U v1):_a(v),_b(v1) {}template <typename T,typename U>
T Blob<T,U>::getA() {return _a;
}template <typename T,typename U>
U Blob<T,U>::getB() {return _b;
}
那么在main.cpp
文件中
#include "blob_test.h"
#include <iostream>
int main() {//Blob<int,std::string>是实例化的类型Blob<int,std::string> b(18,"123");std::cout<<"A:"<<b.getA()<<std::endl;std::cout<<"B:"<<b.getB()<<std::endl;
}
编译blob_test.h
,blob_test.cpp
,main.cpp
将会报错,因为在main.cpp
中只包含了blob_test.h
只有声明,没有定义,编译器无法实例化该模板。
所以对模板,一般将声明与定义都写在头文件,即声明也包含定义。那么blob_test.h
修改如下:
#include <string>
template <typename T,typename U>
class Blob {
public:Blob(T a,U b);
public:T getA();U getB();
private:T _a;U _b;
};//Blob<T,U>就表示一个类
template <typename T,typename U>
T Blob<T,U>::getA() {return _a;
}template <typename T,typename U>
U Blob<T,U>::getB() {return _b;
}template <typename T,typename U>
Blob<T,U>::Blob(T v,U v1):_a(v),_b(v1) {}
模板的设计者应该提供一个头文件,包含模板定义及在类模板或成员定义中用到的所有名字的声明。模板用户必须包含模板的头文件,以及用来实例化模板的任何类型的头问题。
模板的定义与声明,可以看看这篇文章。
函数模板
template <typename T>
int compare(const T& v1,const T& v2) {if (v1 < v2) return -1;if (v2 < v1) return 1;return 0;
}std::cout<<compare(1,0)<<std::endl;
模板定义以关键字template
开始,后跟一个模板参数列表,这是一个逗号分隔的一个或多个模板参数的列表。
函数模板不必显示写出类型实参,编译器可以根据实参推导。比如compare(1,0)
在编译期间就被编译器实例化为一个int
的compare
版本。
一般来说,我们可以将类型参数看作类型说明符,就像内置类型或类型说明符一样使用。
特别是,类型参数可以用来指定返回类型或参数的参数类型,以及在函数体内用于变量声明或类型转换。
template <typename T>
T foo(T *p) {//tmp的类型将是指针p指向的类型T tmp = *p;//...return tmp;
}
非类型模板参数
template<unsigned N,unsigned M>
int compare(const char(&p1)[N],const char(&p2)[M]);
类模板
如下是一个模板类Bolb
的声明。
template <typename T> class Blob {
public:T& back();T& operator[](size_type i);
private:std::shared_ptr<std::vector<T>> _data;static size_t _v;
};
Blob<T>
就当一个正常的类使用**,template <typename T> Blob<T>
就是它的完整形式。
类的成员函数定义
当我们定义一个成员函数时,应如下定义:
template <typename T>
ret-type Blob<T>::member-name(parm-list)
Blob
成员函数定义如下:
template <typename T>
void Blob<T>::check(size_t i,const std::string& msg) const {....;
}
template <typename T>
T& Blob<T>::back() {....
}
构造函数的定义
template <typename T>
Blob<T>::Blob():data(...) {...
}
类的静态成员的定义
template <typename T>
size_t Foo<T>::_v = 0;
类模板的实例化
与函数模板不同之处是,编译器不能为类模板推断模板参数类型,实例化时必须指定类型,如Blob<int>
,Blob<std::string>
。
使用模板类型中的类型成员
当编译器遇到这样的语句 T::size_type *p;
时,它需要指定我们是正在定义一个名为p的变量,还是将一个名为size_type
的static
数据成员与名为p的变量相乘。默认情况下,C++语言假定通过作用域运算符访问的名字不是类型。
因此,如果我们希望使用一个模板类型参数的类型成员,就必须显示告诉编译器该名字是一个类型。我们通过使用关键字typename
来实现这一点:
template <typename T>
typename T::value_type top(const T& c) {if (!c.empty()) {return c.back();} else {return typename T::value_type();}}
默认模板参数
template <typename T, typename F = less<T>>
int compare(const T& v1,const T& v2, F f= F()) {...
}
template <class T = int>
class Numbers {....
};
指定显示模板实参(函数模板显示实参)
我们可以定义表示返回类型的第三个模板参数,从而允许用户控制返回类型:
template <typename T1,typename T2,typename T3>
T1 sum(T2,T3);
T1
指定返回值的类型。
实例化时,返回值的类型被显示指定
// T1是显示指定,T2和T3是从函数实参类型推断而来
auto val3 = sum<long long>(i,lng);
引用折叠和右值引用参数
如下一个模板函数f(T&&)
,注意参数类型的推导。
template <typename T> void f(T &&);
f3(42);//42是一个右值,那么模板参数T被推导为int
int i = 118;
f3(i);//i是个左值,那么模板参数T被推导为int&
如下例子,函数f
将改变量i
的值,因为类型被推导为int&
。
#include <iostream>
template <typename T> void f(T&& v) {v = 18;
}int main() {int i = 0;f(i);std::cout<<"i:"<<i<<std::endl;
}
当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数(如T&&
)时,编译器推断模板类型参数为实参的左值引用类型。
因此,当我们调用f3(i)
时,编译器推断T的类型为int&
,而非int
。
那么对f3(i)
,编译器实际推导为:
void f3<int&>(int& &&);
T
的类型为int&
,触发了C++中的引用折叠。
引用折叠的规则如下:
X& &
,X& &&
和X&& &
都折叠成类型X&
。- 类型
X&& &&
折叠成X&&
。
那么void f3<T>(T &&)
的模板参数可以称为万能引用,因为实参既可以传入左值,也可以传入右值:
f(i);
f(18);
可变参数模板
可以传入多个实参,像printf
一样。
template <typename T,typename ...Args>
void foo(const T &t,const Args& ...rest);
对参数包的扩展
template <typename... Args>
ostream &errorMsg(ostream &os,const Args&... rest) {return print(os,debug_rep(rest)...);
}
对参数包rest进行了扩展,将对每个参数都调用debug_rep
。
对参数包的转发
//...Args表示模板参数的类型
//Args&&... 表示以Args声明了形参args
template<typename ...Args>
void fun(Args&&... args) {work(std::forward<Args>(args)...);
}
可变参数模板的示例
#include <iostream>
void work(int a,int b) {std::cout<<"===> enter work(int a,int b)"<<std::endl;std::cout<<"a:"<<a<<",b:"<<b<<std::endl;std::cout<<"===> exit work(int a,int b)"<<std::endl;
}void work(int& a,int& b) {std::cout<<"===> enter work(int& a,int& b)"<<std::endl;a = 1;b = 2;std::cout<<"===> exit work(int& a,int& b)"<<std::endl;
}void work(const int&a,const int& b) {std::cout<<"===> enter work(const int&a,const int& b)"<<std::endl;std::cout<<"===> exit work(const int&a,const int& b)"<<std::endl;
}template <typename ...Args>
void foo(Args&& ...v) {work(std::forward<Args>(v)...);
}int main() {//模板参数的实参类型//类型被推断为int//此时会报错,因为选择const int&和int,int&都可以foo(18,118);int a=16,b=116;/*模板参数会被推断为int&,此时会有引用折叠,但是通过forward*将类型保留转发到了work。*此时 int&,const int&都适合,但是int&更加合适,会选择int&。*/foo(a,b);std::cout<<"a:"<<a<<",b:"<<b<<std::endl;
}
模板参数传入可调用对象
#include <iostream>template<typename Fun,typename ...Args>
void Test(Fun _fun,Args&&... args) {_fun(std::forward<Args>(args)...);
}//函数对象
struct SWork {void operator()(int a,int b) {std::cout<<"SWork,a:"<<a<<",b:"<<b<<std::endl;}
};//函数
void work(int a,int b) {std::cout<<"work,a:"<<a<<",b:"<<b<<std::endl;
}int main() {Test(work,18,18);Test(SWork(),19,19);
}