一、函数模板初探

1.定义模板

1
2
3
4
template <typename T>
T max(T a, T b) {
return a > b ? a : b;
}

在这个例子中template<>角括号中是使用逗号分隔的参数列表,template是类型参数的引导字,这是目前为止最典型的模板参数用法,但模板也可以使用除类型外的其他参数,我们将在之后进行介绍。

需要注意的两点是:1.使用的类型T必须支持>号操作。2.因为采用按值返回,所以T的类型也必须是可拷贝的。

鉴于历史原因,可以使用class关键字代替typename,因为typename一词在C++98标准中引进,由于历史原因便保留了class 关键字,但要注意这不是类的概念,所以不能使用struct替代typename

2.使用模板

1
2
3
4
int a1 = 1, a2 = 2;
std::cout << ::max(a1, a2) << "\n";
double b1 = 1.1, b2 = 2.2;
std::cout << ::max(b1, b2) << "\n";

上述代码中,::max()分别传入intdouble两个类型的参数,并返回两者最大值(这里使用::是考虑到std标准库中也含有一个std::max()函数,防止冲突),注意模板不会被编译成可以处理任何类型的单一实体。在编译期,模板会根据传入参数的类型不同,而生成相应的实体,因此对于上述代码,模板生成了int max()double max()两个实体,替代了原来的模板函数,这一过程被称为模板的实例化

3.两阶段编译

(1).定义期间,模板不会实例化,该阶段不会检查调用类型是否可以合法地进行函数内的操作,只能发现诸如语法错误或者独立于模板的静态断言错误。

(2).实例化期间会再次检查模板代码,确保所有代码的有效性。

编译和链接:

当使用模板触发其实例化的时候,编译器需要查看模板的定义,这就意味着模板的定义并不能像普通函数一样使用头文件和源文件,而是必须定义在头文件中,我们约定俗成为.hpp

二、模板实参推导

1
2
3
4
5
template <typename T>
T max(T const &a, T const &b)
{
return a > b ? a : b;
}

在第一部分我们使用int参数时,模板可以推导出T为int类型,但看上述代码,若传入参数为常引用类型,再次使用int传参,编译能否成功?

答案是肯定的,int类型调用了常引用的::max()版本,并且T依旧被推到为int类型,这说明T可能只是类型的部分体现。

1.类型推导中的类型转换

(1).当声明调用参数按引用类型传递时,禁用类型推导,使用相同类型T的两个传入参数类型必须匹配。

(2).当声明调用参数为按值传递时,仅支持退化的简单转换:忽略constvolatile等限定符,引用转换为引用类型,原始数组或函数转换为指针类型,且经退化后的两个参数类型必须匹配。

1
2
3
4
5
const int c1 = 1;
int c2 = 2;
::max(c1, c2) // 可以运行,T被推导为int
double c3 = 3.14;
::max(c2, c3) // 错误,无法进行类型转换

解决上述错误的方法有三种:

(1).强制类型转换

1
::max(static_cast<double>c2 c3);

(2).显式指定T的类型,阻止编译器自动类型推导

1
::max<double>(c2, c3);

(3).指定不同类型的类型参数

2.默认实参的类型推导

类型推导对默认调用实参不起作用:

1
2
3
4
template <typename T>
void func(T = " ") {}
// code
f(); // error,无法用默认类型进行推导

解决方法:对模板参数也同样声明一个默认实参,与调用实参相匹配:

1
2
3
4
template <typename T = std::string>
void func(T = " ") {}
// code
f(); // ok

三、多模板参数

1.返回类型的模板参数

1
2
3
4
5
template <typename RT, typename T1, typename T2>
RT max(T1 a, T2 b)
{
return a > b ? a : b;
}

由于模板无法针对返回类型做出推到,因此我们必须指定返回类型:

1
::max<double>(1, 2.2);

可以只指定第一个或者依次显示指定实参类型,直到不存在不能隐式推导的实参类型为止。

2.推导返回类型

使用auto

1
2
3
4
5
template <typename T1, typename T2>
auto max(T1 a, T2 b)
{
return a > b ? a : b;
}

看起来是最简单的方案,但这样做无法得到一个统一的返回类型,也就是说可能发生隐式的类型转换,产生不必要的麻烦。

更为严重的问题是,如果我们选择这样做,必须存在能够推导出返回类型的返回语句,并且所有返回语句推到的返回类型必须相匹配,否则程序就会报错:

1
2
3
4
5
6
template <typename T1, typename T2>
auto max(const T1 &&a, const T2 &&b)
{
return a > b ? a : b;
return int(3);
}

像这样,即时明显下面的return语句永远不会被调用,但在编译期它仍会被考虑在内,程序会在编译期报错。

auto+尾置返回类型:

1
2
3
4
5
template <typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(a > b ? a : b)
{
return a > b ? a : b;
}

在C++11中,我们能够使用形参的尾置返回类型语法来声明返回类型。如使用decltype(),这样做会找到两种返回值的公共算数类型。事实上,如果把其中的a > b改为true即可,在编译期即可找到它们的公共类型。

std::decay<>

上面两种做法无论何时都有一个显著的缺点:传入参数T可能是引用类型,此时我们应该返回T的退化类型,而不是引用类型。

使用类型特征std::decay<>,他的返回结果为成员type:

1
2
3
4
5
template <typename T1, typename T2>
auto max(T1 a, T2 b) -> typename std::decay<decltype(true ? a : b)>::type
{
return a > b ? a : b;
}

common_type萃取公共类型:

1
2
3
4
5
6
#include <type_traits>
template <typename T1, typename T2>
typename std::common_type<T1, T2>::type max(T1 a, T2 b)
{
return a > b ? a : b;
}

1
2
3
4
5
6
#include <type_traits>
template <typename T1, typename T2>
std::common_type_t<T1, T2> max(T1 a, T2 b)
{
return a > b ? a : b;
}

common_type会萃取两种(或更多)类型的公共类型,并且支持退化操作。

四、传值与传引用的选择

通常情况下,除了占用内存非常少的简单类型(如基本类型和std::string_view)之外,建议所有类型按引用传递,避免非必要的拷贝操作。

但是若有以下几个原因,传值更优:
1.编译优化更好。

2.移动语义拷贝成本更低。

3.有时完全没有拷贝或移动。

4.有时传引用会面临风险。