C++templates第一章函数模板

为什么要使用模板?

C++要求我们要用特定的类型来声明变量,函数以及其他一些内容。这样很多代码可能就只是处理的变量类型有所不同。比如对不同的数据类型,quicksort 的算法实现在结构上可能完全一样,不管是对整形的 array,还是字符串类型的 vector,只要他们所包含的内容之间可以相互比较。

如果你所使用的语言不支持这一泛型特性,你将可能只有如下糟糕的选择:

  1. 你可以对不同的类型一遍又一遍的实现相同的算法。
  2. 你可以在某一个公共基类(commonbasetype,比如 Object 和 void*)里面实现通用的算法代码。
  3. 你也可以使用特殊的预处理方法。

如果你是从其它语言转向 C++,你可能已经使用过以上几种或全部的方法了。然而他们都各有各的缺点:

  1. 如果你一遍又一遍地实现相同算法,你就是在重复地制造轮子!你会犯相同的错误,而 且为了避免犯更多的错误,你也不会倾向于使用复杂但是很高效的算法。
  2. 如果在公共基类里实现统一的代码,就等于放弃了类型检查的好处。而且,有时候某些 类必须要从某些特殊的基类派生出来,这会进一步增加维护代码的复杂度。
  3. 如果采用预处理的方式,你需要实现一些“愚蠢的文本替换机制”,这将很难兼顾作用域和类型检查,因此也就更容易引发奇怪的语义错误。

而模板这一方案就不会有这些问题。模板是为了一种或者多种未明确定义的类型而定义的函数或者类。在使用模板时,需要显式地或者隐式地指定模板参数。由于模板是 C++的语言特 性,类型和作用域检查将依然得到支持。

函数模板初探

函数模板提供了适用于不同数据类型的函数行为,也就是说,函数模板代表的是一组函数,除了某些信息未被明确指定之外,他们看起来很像普通函数,这些没被指定的信息就是被参数化的信息。

定义模板

1
2
3
4
5
6
template<typename T> 
T max (T a, T b)
{
// 如果b < a, 返回a,否则返回b
return b < a ? a : b;
}

这个模板定义了一组函数,他们都返回函数的两个参数中值较大的那一个。这两个参数的类型并没有被明确指定,而是被表示为模板参数T。模板参数必须按照如下语法声明:

template<由逗号分隔的模板参数>

在我们的例子中,模板参数是typename T。类型参数可以代表任何类型,他在模板被调用的时候决定。但是这个类型(可以是基础类型、类或其他类型)应该支持模板用到的运算符,在本例中,类型T必须支持<运算符。

typename可以用class代替,但是应该优先使用typename。

使用模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "max1.hpp" 
#include <iostream>
#include <string>
int main()
{
int i = 42;
std::cout << "max(7,i): " << ::max(7,i) << ’\n’;
double f1 = 3.4; double f2 = -6.7;
std::cout << "max(f1,f2): " << ::max(f1,f2) << ’\n’;
std::string s1 = "mathematics";
std::string s2 = "math";
std::cout << "max(s1,s2): " << ::max(s1,s2) << ’\n’;
}

在这段代码中,max()被调用了三次:一次是比较两个 int,一次是比较两个 double,还有一次是比较两个 std::string。每一次都会算出最大值。

下面是输出结果:

max(7,i): 42

max(f1,f2): 3.4

max(s1,s2): mathematics

注意在调用 max()模板的时候使用了作用域限制符::。这样程序将会在全局作用域中查找 max()模板。否则的话,在某些情况下标准库中的 std::max()模板将会被调用,或者有时候不太容易确定具体哪一个模板会被调用。
在编译阶段,模板并不是被编译成一个可以支持多种类型的实体。而是对每一个用于该模板 的类型都会产生一个独立的实体。因此在本例中,max()会被编译出三个实体,因为它被用 于三种类型。比如第一次调用时,等效于如下函数:

1
2
3
4
int max (int a, int b) 
{
return b < a ? a : b;
}

模板参数推断

当我们调用形如 max()的函数模板来处理某些变量时,模板参数将由被传递的调用参数决定。 如果我们传递两个 int 类型的参数给模板函数,C++编译器会将模板参数 T 推断为 int。

不过 T 可能只是实际传递的函数参数类型的一部分。比如我们定义了如下接受常量引用作为 函数参数的模板:

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

此时如果我们传递 int 类型的调用参数,由于调用参数和 intconst&匹配,类型参数 T 将被推断为int

类型推断中的类型转换

在类型推断的时候自动的类型转换是受限制的:

  • 如果调用参数是按引用传递的,任何类型转换都不被允许。通过模板类型参数 T 定义的 两个参数,它们实参的类型必须完全一样
  • 如果调用参数是按值传递的,那么只有退化(decay)这一类简单转换是被允许的: const 和 volatile 限制符会被忽略,引用被转换成被引用的类型,rawarray 和函数被转换为相应的指针类型。通过模板类型参数 T 定义的两个参数,它们实参的类型在退化(decay) 后必须一样
1
2
3
4
5
6
7
8
9
10
11
template<typename T>
T max (T a,T B);

int const c = 42;
int i = 1; //原书缺少i的定义
max(i, c); // OK: T被推断为int,c中的const被decay掉
max(c, c); // OK: T被推断为int
int& ir = i;
max(i, ir); // OK: T被推断为int, ir中的引用被decay掉
int arr[4];
foo(&i, arr); // OK: T被推断为int*

但是这样就是错误的:

但是像下面这样是错误的:

1
2
3
max(4, 7.2); // ERROR: 不确定T该被推断为int还是double
std::string s;
foo("hello", s); //ERROR: 不确定T该被推断为 const[6] 还是 std::string

有三种办法解决以上错误:

  1. 对参数做类型转换 max(static_cast(4), 7.2); // OK
  2. 显式地指出类型参数 T 的类型,这样编译器就不再会去做类型推导。 max(4, 7.2); // OK
  3. 指明调用参数可能有不同的类型(多个模板参数)。

对默认调用参数的类型推断

需要注意的是,类型推断并不适用于默认调用参数,例如:

1
2
3
4
template<typename T> 
void f(T = "");
f(1); // OK: T被推断为int, 调用 f<int> (1)
f(); // ERROR: 无法推断T的类型

为应对这一情况,你需要给模板类型参数也声明一个默认参数,1.4 节会介绍这一内容:

1
2
3
template<typename T = std::string> 
void f(T = "");
f(); // OK

多个模板参数

目前我们学习到了与函数模板相关的两组参数:

  1. 模板参数,定义在函数模板前面的尖括号里

    template<typename T>

  2. 调用参数,定义在函数模板名称后面的圆括号里

    T max(T a,T b)

参数参数可以是一个或者多个,如下所示:

1
2
3
4
5
6
template<typename T1, typename T2> 
T1 max (T1 a, T2 b)
{
return b < a ? a : b;
}
auto m = ::max(4, 7.2); // OK, 但是返回类型是第一个模板参数T1的类型

看上去如你所愿,它可以接受两个不同类型的调用参数。

但是如示例代码所示,这也导致了一个问题。如果你使用其中一个类型参数的类型作为返回类型,不管是不是和调用者预期地一样,当应该返回另一个类型的值的时候,返回值会被做类型转换。这将导致返回值的具体类型和参数的传递顺序有关。

如果传递 66.66 和 42 给这个函数模板,返回值是 double 类型 的 66.66,但是如果传递 42 和 66.66,返回值却是 int 类型的 66。

  • C++提供了多种应对这一问题的方法:
    1. 引入第三个模板参数作为返回类型。
    2. 让编译器找出返回类型。
    3. 将返回类型定义为两个参数类型的“公共类型”

作为返回类型的模板参数

按照之前的讨论,模板类型推断允许我们像调用普通函数一样调用模板函数模板,我们可以不去显式的指出模板参数的类型

但是也提到,我们也可以显式地指出模板参数的类型:

1
2
3
4
template<typename T>
T max(T a,T b);

::max<double>(4,7.2);

当模板参数和调用参数没有必然的联系且模板参数不能确定的时候,我们可以显式指明模板参数,可以引入第三个模板来指定函数模板的返回类型:

1
2
3
4
template<typename T1, typename T2, typename RT> 
RT max (T1 a, T2 b);

::max<int,double,double>(4,7.2);

这样做是可以的,但是太繁琐。

目前我们看到的情况是,要么所有模板参数都被显式指定,要么一个都不指定,另一个办法是只指定第一个模板参数的类型,其余参数的类型通过推断获得,通常而言,我们必须显式指定所有模板参数的类型,因此我们调换一下模板参数顺序,就可以调用时只指定返回值的类型:

1
2
3
4
template<typename RT, typename T1, typename T2> 
RT max (T1 a, T2 b);

::max<double>(4, 7.2) //OK: 返回类型是double,T1和T2根据调用参数推断

返回类型推断

如果返回类型是由模板参数决定的,那么推断返回类型最简单也是最好的办法就是让编译器 来做这件事。从 C++14 开始,这成为可能,而且不需要把返回类型声明为任何模板参数类型 (不过你需要声明返回类型为 auto):

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

事实上,在不使用 尾 置 返 回 类 型 ( trailingreturntype ) 的情况下将 auto 用于返回类型,要求返回类型必须能够通过函数体中的返回语句推断出来。当然,这首先要求返回类型能够从函数体中推断出来。因此,必须要有这样可以用来推断返回类型的返回语句,而且多个返回语句之间的推断结果必须一致。

在 C++14 之前,要想让编译器推断出返回类型,就必须让或多或少的函数实现成为函数声明的一部分。在 C++11 中,尾置返回类型(trailingreturntype)允许我们使用函数的调用参数。 也就是说,我们可以基于运算符?:的结果声明返回类型:

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

但是在某些情况下会有一个严重的问题:由于 T 可能是引用类型,返回类型就也可能被推断 为引用类型。因此你应该返回的是 decay 后的 T,像下面这样:

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

把返回类型声明为公共类型

从 C++11 开始,标准库提供了一种指定“更一般类型”的方式。std::common_type<>::type产生的类型是他的两个模板参数的公共类型。比如:

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 b < a ? a : b;
}

同样的,std::common_type 也是一个类型萃取(typetrait),定义在中,它返回一个结构体,结构体的 type 成员被用作目标类型。因此其主要应用场景如下:

typename std::common_type<T1,T2>::type //since C++11

不过从 C++14 开始,你可以简化“萃取”的用法,只要在后面加个_t,就可以省掉 typename 和::type(参见 2.8 节),简化后的版本变成:

std::common_type_t<T1,T2> // equivalent since C++14

std::common_type<>的实现用到了一些比较取巧的模板编程手法,具体请参见 25.5.2 节。它 根据运算符?:的语法规则或者对某些类型的特化来决定目标类型。因此::max(4, 7.2) 和::max(7.2,4)都返回 double 类型的 7.2. 需要注意的是,std::common_type<>的结果也是退化的。

默认模板参数

你也可以给模板参数指定默认值。这些默认值被称为默认模板参数并且可以用于任意类型的模板。它们甚至可以根据其前面的模板参数来决定自己的类型。

比如如果你想将前述定义返回类型的方法和多模板参数一起使用,你可以为返回类型引入一 个模板参数 RT,并将其默认类型声明为其它两个模板参数的公共类型。同样地,我们也有多种实现方法:

  • 我们可以直接使用运算符?:。不过由于我们必须在调用参数 a 和 b 被声明之前使用运算 符?:,我们只能像下面这样:

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

    请注意在这里我们用到了 std::decay_t<>来确保返回的值不是引用类型。
    同样值得注意的是,这一实现方式要求我们能够调用两个模板参数的默认构造参数。还有另 一种方法,使用 std::declval,不过这将使得声明部分变得更加复杂

  • 我们也可以利用类型萃取 std::common_type<>作为返回类型的默认值:

    1
    2
    3
    4
    5
    6
    #include <type_traits> 
    template<typename T1, typename T2, typename RT = std::common_type_t<T1,T2>>
    RT max (T1 a, T2 b)
    {
    return b < a ? a : b;
    }
  • 我们希望能够将返回类型作为第一个模板参数,并且依然能够从其它两个模板参数推断出它的类型:

    1
    2
    3
    4
    5
    template<typename RT = long, typename T1, typename T2> 
    RT max (T1 a, T2 b)
    {
    return b < a ? a : b;
    }
    1
    2
    3
    4
    5
    int i; 
    long l;
    // ...
    max(i, l); // 返回值类型是long (RT 的默认值)
    max<int>(4, 42); //返回int,因为其被显式指定

函数模板的重载

像普通函数一样,模板也是可以重载的。也就是说,你可以定义多个有相同函数名的函数, 当实际调用的时候,由 C++编译器负责决定具体该调用哪一个函数。即使在不考虑模板的时候,这一决策过程也可能异常复杂。

下面几行程序展示了函数模板的重载:

1
2
3
4
5
6
7
8
9
// maximum of two int values: 
int max (int a, int b)
{
return b < a ? a : b;
} // maximum of two values of any type:
template<typename T>
T max (T a, T b) {
return b < a ? a : b;
}

测试:

1
2
3
4
5
6
7
8
9
int main() 
{
::max(7, 42); // calls the nontemplate for two ints
::max(7.0, 42.0); // calls max<double> (by argument deduction)
::max(’a’, ’b’); //calls max<char> (by argument deduction)
::max<>(7, 42); // calls max<int> (by argumentdeduction)
::max<double>(7, 42); // calls max<double> (no argumentdeduction)
::max(’a’, 42.7); //calls the nontemplate for two ints
}

在其他因素相同的情况下,模板解析过程将优先选择非模板函数,而不是从模板实例化出来的函数。

由于在模板参数推断时不允许自动类型转换,而常规函数是允许的,因此最后一个调用会选择非模板参函数(‘a’和 42.7 都被转换成 int):

要保证对任意一个调用,都只会有一个模板匹配

讨论

按值传递还是按引用传递

你可能会比较困惑,为什么我们声明的函数通常都是按值传递,而不是按引用传递。通常而言,建议将按引用传递用于除简单类型(比如基础类型和 std::string_view)以外的类型,这样可以免除不必要的拷贝成本。

不过出于以下原因,按值传递通常更好一些:

  • 语法简单。
  • 编译器能够更好地进行优化。
  • 移动语义通常使拷贝成本比较低。
  • 某些情况下可能没有拷贝或者移动。

再有就是,对于模板,还有一些特有情况:

  • 模板既可以用于简单类型,也可以用于复杂类型,因此如果默认选择适合于复杂类型可 能方式,可能会对简单类型产生不利影响。
  • 作为调用者,你通常可以使用 std::ref()和 std::cref()(参见 7.3 节)来按引用传递参数。
  • 虽然按值传递 stringliteral 和 rawarray 经常会遇到问题,但是按照引用传递它们通常只 会遇到更大的问题。第 7 章会对此做进一步讨论。在本书中,除了某些不得不用按引用传递的地方,我们会尽量使用按值传递。

为什么不适用inline

通常而言,函数模板不需要被声明成 inline。不同于非 inline 函数,我们可以把非inline的函数模板定义在头文件里,然后在多个编译单元里 include 这个文件。
唯一一个例外是模板对某些类型的全特化,这时候最终的 code 不在是“泛型”的(所有的 模板参数都已被指定)。详情请参见 9.2 节。
严格地从语言角度来看,inline只意味着在程序中函数的定义可以出现很多次 。不过它也给了编译器一个暗示,在调用该函数的地方函数应该被展开成 inline的:这样做在某些情况下可以提高效率,但是在另一些情况下也可能降低效率。现代编译器在没有关键字inline暗示的情况下,通常也可以很好的决定是否将函数展开成 inline 的。当然,编译器在做决定的时候依然会将关键字 inline 纳入考虑因素。

总结

  • 函数模板定义了一组适用于不同类型的函数。
  • 当向模板函数传递变量时,函数模板会自行推断模板参数的类型,来决定去实例化出那 种类型的函数。
  • 你也可以显式的指出模板参数的类型。
  • 你可以定义模板参数的默认值。这个默认值可以使用该模板参数前面的模板参数的类型,而且其后面的模板参数可以没有默认值。
  • 函数模板可以被重载。
  • 当定义新的函数模板来重载已有的函数模板时,必须要确保在任何调用情况下都只有一个模板是最匹配的。
  • 当你重载函数模板的时候,最好只是显式地指出了模板参数的类型。
  • 确保在调用某个函数模板之前,编译器已经看到了相对应的模板定义。