模板与泛型(一)


函数模板

函数模板定义为

template<class 参数1, class 参数2,...>
返回值类型 模板名(形参表)
{
	函数体
}

函数模板可以重载,只要它们形参表不同即可,下面两个模板可以同时存在:

template<class T1, class T2>
void print(T1 arg1, T2 arg2){
	cout << arg1 << "" << arg2<<endl;
}
template<class T>
void print(T arg1, T arg2){
	cout << arg1 << "" <<arg2 <<endl;
}

当我们调用函数模板时,编译器会对函数的实参进行类型推断,从而得出模板参数T的类型,这个过程称为模板的实例化(instantiate)。当编译器实例化一个模板后,得到的函数是一个有确定签名的函数,这个函数称为模板的实例(instance)

函数模板匹配规则

C++编译器如何决定选用选用哪个函数,遵循以下优先顺序

  1. 先找参数完全匹配普通函数(非由模板实例化而得的函数)
  2. 再找参数完全匹配的模板函数
  3. 再找实参经过自动类型转换后能够匹配的普通函数
  4. 上面的都找不到,则报错
template<class T>
T max(T a, T b){
	return 0;
}
template<class T1, clas T2>
T max(T1 a, T2 b){
	return 0;
}
double max(double a, double b){
	return 0;
}

int main(){
	int i=4, j=5;
	max(1.2,3.5);//调用max(double, double)
	max(i,j);//调用第一个max函数
	max(1.3,2);//调用第二个max函数
	return 0;
}

非类型模板参数

除了在函数模版中定义模板类型参数外,还可以在模板中定义非类型参数

当模板被实例化时,这些参数会被自动推断出来,例如下面例子中,compare函数用来比较两个字符数组,由于数组不能拷贝,因此参数为两个数组的引用,数组的长度用两个非类型参数表示:

tempplate<unsigned N, unsigned M>
compare(const char(&p1)[N], const char(&sp2)[M]){
	return strcmp(p1,p2);
}
compare("h1","h11")

当调用compare时,编译器会推断出NM的值来实例化模板,上述函数模板会被编译器实例化为

compare(const char(&p1)[3], const char(&sp2)[4]) //考虑\0

使用非类型的模板参数,需要注意的是这些参数只能是值或者常量表达式

inline与const

函数模板可以被声明为inline的,inline说明符放到模板参数列表之后

template <typename T>
inline T min(const T& , const T&);

另一个需要注意的点是,可以将函数模板的参数声明为const用来满足一些不支持拷贝的数据类型。

显示实参

在某些情况下,编译器无法推断出模板的实参类型,比如当函数的返回类型于参数列表中任何类型都不同时,此时需要用户来手动指定模板参数的类型。

template<typename T1, typename T2, typename T3>
T1 sum(T2 x1, T3 x2);

上述代码中,编译器可以根据出x1x2来推测出T2T3的类型,但是没法推测出T1的类型,因此需要调用者手动指定

auto val = sum<long long>(i, lng); //long long sum(int, long)

此时,我们告诉编译器T1long long,而T2T3编译器可自行推断出。

显示指定的参数类型是从左到右依次展开,即

auto val = sum<long long, int, long>(i, lng);

但是如果T3的类型可以由T1T2推导出,我们此时可以用尾置返回类型

template <typename It>
auto &fn (It beg, It end) -> decltype(*beg)

vector<int> v1 = {1,2,3,4,5};
vector<string> v2 = {"hi","bye"};
auto &i = fn(v1.begin(), v2.begin()); //fn应该返回int&
auto &j = fn(v2.begin(), v2.begin()); //fn应该返回string&

此时fn函数的返回值类型可由迭代器类型It推导出来,因此我们不必手动指定其类型。

如果我们继续追问让上述函数返回一个“值”而非引用时,decltype(*beg)就不奏效了,因为迭代器永远返回引用类型。此时如果想要返回值类型,我们需要使用type_traits。具体来说,有两种办法

  • C++11中可以使用remove_reference
  • C++14中可以使用std::decay
//c++ 11
template <typename It>
auto fn (It beg, It end) -> typename std::remove_reference<decltype(*beg)>::type {
	return *beg;
}

//c++14
template <typename It>
auto fn (It beg, It end) -> typename std::decay<decltype(*beg)>::type {
	return *beg;
}

函数指针

我们可以用函数模板来初始化函数指针,编译器将用函数指针的类型来推断模板参数的类型

template<typename T>
int compare(const T&, const T&);
int (*fp1)(const int&, const int&) = compare;

此时compare中的T将实例化为int类型,fp1则指向实例化后的compare函数。同样,我们可以将compare的一个特例当做参数传给某个函数

func(compare<int>);

类模板

类模板与函数模板类似,是一种泛型技术,但是和函数模板不同的是,类模板的类型参数无法靠编译器推断,必须由使用者指定。类模板的定义方式如下

template<类型参数表>
class 类模板名
{
	成员函数和成员变量
};

其中,类型参数表可以有多个参数,比如class T, class M,...。本节中,我们以一个Blob类为例

template <typename T>
class Blob{
    typedef typename std::vector<T>::size_type size_type;
private:
    std::shared_ptr<vector<T>> data;
public:
    Blob():data(std::make_shared<vector<T>>()){};
    Blob(std::initializer_list<T> il):data(std::make_shared<vector<T>>(il)){}
    size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }
    void push_back(const T& ele){ data->push_back(ele);}
    void push_back(T&& ele){ data->push_back(ele);}
    void pop_back();
    T& back();
    T& operator[](size_type i);
};

上面代码中,我们定义了一个Blob的类模板,由类模板实例化得到的类叫模板类。例如,我们对Blob模板进行int型的实例化,则会产生一个新的模板类Blob<int>,相应的,编译器会为其生成如下代码

template<>
class Blob<int>{
private:
	std::shared_ptr<vector<int>> data;
public:
	...
	Blob(std::initializer_list<int> il);
	...
	int& operator[](size_type i);
}

上述代码形式也叫做模板的特例化,我们在后面还会讨论这种形式。需要注意的是,同一个类模板的两个模板类是不兼容的。比如:

Blob<int>
Blob<string>

如果类模板中有默认参数,则构造该类对象时无需传入任何参数

template <class T = int>
class Numbers {
public:
	Numbers(T v = 0):val(v){}
private:
	T val;
};
Numbers<long> x1; //用long替换T
Numbers<> x2; //使用默认的int,此时x2是int型

通常,我们在调用函数时有声明就够了,声明包含了这个函数的签名信息以及符号。类似的,当我们使用一个类对象时,类对象的定义必须是可用的,但成员函数的定义可不需要,道理和普通函数相同。因此,在C++中我们可以将类定义和函数声明放在头文件中,而将普通函数的定义和成员函数的定义放在源文件中。

对于模板类或者函数,情况则不一样,为了实例化一个模板,编译器必须要了解函数的定义才能推断出类型参数。因此,函数模板或类模板成员函数的定义通常放在头文件中。对于在源文件中定义的成员函数,则要使用下面语法:

template<形参表>
返回值类型 类模板名<类型参数名列表>::成员函数名(参数表){}

例如,我们可以继续实现back,pop_back方法和[]符号重载

template<typename T>
T& Blob<T>::back(){
    return data->back();
}
template<typename T>
T& Blob<T>::operator[](size_type i){
    return (*data)[i];
}
template<typename T>
void Blob<T>::pop_back(){ data->pop_back(); }

最后,如果在一个类模板中出现该类本身,则可以忽略类型参数,例如下面代码

template <typename T>
class Blob{
	//...
    Blob getBlob(){ //此处可直接返回Blob,而不必使用Blob<T>
        return Blob();
    }
}

类成员模板

上面提到了模板类中的成员函数和类模板共用参数T的情况,实际上对于成员函数来说,它们也可以拥有自己的模板参数类型。此时又可以分为两种情况,一种是一个普通类,它的成员函数是模板,另一种是上面提到的模板类,但是它的成员函数的模板类型和类模板不同

我们先说第一种情况,这种情况比较简单,相当于定义一个普通的函数模板

class DebugDelete {
public:
	DebugDelete(std::ostream &s = std::cerr):os(s){}
	tempalte <typename T>
	void operator()(T* p) const {
		os<<"deleting unique ptr"<<std::endl;
		delete p;
	}
private:
	std::ostream &os;
}

使用方式也和普通的模板函数相同,对于不同的参数类型会实例化出不同的函数模板,比如

double *p = new double;
DebugDelete d;
d(p); //void DebugDelete::operator()<double>(double* p) const;

对于第二种情况,我们以下面代码为例

template<typename T>
struct Obj {
    template <typename U>
    void func(U x) {
        //implementation
    };
};

int main(){
    Obj<int> o;
    o.func<float>(10.0f);
    return 0;
}

上面例子中我们分别传入了不同的模板参数,编译器可以正确推断出模板的类型。但是有些情况编译器将无法得到正确结果,比如下面例子

template <typename T>
void f1(T x) {
    Obj<T> o;
    o.func<T>(x);
}

此时我们在一个函数模板中创建了一个Obj对象,并调用它的func方法,此时编译器将无法知道T是一个类型还是一个变量名,默认情况下<>会被当做小于和大于号进行处理,因此上述代码编译器会理解为

(o.func<T) > (x)

为了解决这个问题,我们需要明确的告诉编译器T是一个模板参数,具体做法是使用.template

o.tempate func<T>(x);

类模板的static成员

类模板可以有static成员,比如下面的类模板:

template<typename T> 
class Foo {
public:
	static std::size_t count() {
		return ctr;
	}
private:
	static std::size_t ctr;
};

template<typename T>
size_t Foo<T>::ctr = 0; //每个`static`成员在定义时必须初始化:

每个Foo的实例都有其自己的static成员实例。即对任意给定的X都有一个Foo<X>::ctr和一个Foo<X>::count成员。所有的Foo<X>类型的对象共享相同的ctr对象和count函数。

Foo<int> f1,f2,f3; //f1,f2,f3享有共同的`Foo<int>::ctr`和`Foo<int>::count()`

Foo<int> fi; //实例化Foo<int>类和static成员ctr
auto ct = Foo<int>::count(); //实例化Foo<int>::count
ct = fi.count(); //等价调用 

类似任何其他成员函数,`static`成员函数只有在使用时才会被实例化

Resources