模板的优化


配套视频课程已更新完毕,大家可通过以下两种方式观看视频讲解:

关注公众号:爱编程的大丙,或者进入大丙课堂学习。


1. 模板的右尖括号

在泛型编程中,模板实例化有一个非常繁琐的地方,那就是连续的两个右尖括号(>>)会被编译器解析成右移操作符,而不是模板参数表的结束。我们先来看一段关于容器遍历的代码,在创建的类模板Base中提供了遍历容器的操作函数traversal():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// test.cpp
#include <iostream>
#include <vector>
using namespace std;

template <typename T>
class Base
{
public:
void traversal(T& t)
{
auto it = t.begin();
for (; it != t.end(); ++it)
{
cout << *it << " ";
}
cout << endl;
}
};


int main()
{
vector<int> v{ 1,2,3,4,5,6,7,8,9 };
Base<vector<int>> b;
b.traversal(v);

return 0;
}

如果使用C++98/03标准来编译上边的这段代码,就会得到如下的错误提示:

1
2
test.cpp:25:20: error: '>>' should be '> >' within a nested template argument list
Base<vector<int>> b;

根据错误提示中描述模板的两个右尖括之间需要添加空格,这样写起来就非常的麻烦,C++11改进了编译器的解析规则,尽可能地将多个右尖括号(>)解析成模板参数结束符,方便我们编写模板相关的代码。

上面的这段代码,在支持C++11的编译器中编译是没有任何问题的,如果使用g++直接编译需要加参数-std=c++11

1
$ g++ test.cpp -std=c++11 -o app

2. 默认模板参数

C++98/03标准中,类模板可以有默认的模板参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

template <typename T=int, T t=520>
class Test
{
public:
void print()
{
cout << "current value: " << t << endl;
}
};

int main()
{
Test<> t;
t.print();

Test<int, 1024> t1;
t1.print();

return 0;
}

但是不支持函数的默认模板参数,在C++11中添加了对函数模板默认参数的支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;

template <typename T=int> // C++98/03不支持这种写法, C++11中支持这种写法
void func(T t)
{
cout << "current value: " << t << endl;
}

int main()
{
func(100);
return 0;
}

通过上面的例子可以得到如下结论:当所有模板参数都有默认参数时,函数模板的调用如同一个普通函数。但对于类模板而言,哪怕所有参数都有默认参数,在使用时也必须在模板名后跟随<>来实例化。

另外:函数模板的默认模板参数在使用规则上和其他的默认参数也有一些不同,它没有必须写在参数表最后的限制。这样当默认模板参数和模板参数自动推导结合起来时,书写就显得非常灵活了。我们可以指定函数模板中的一部分模板参数使用默认参数,另一部分使用自动推导,比如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <string>
using namespace std;

template <typename R = int, typename N>
R func(N arg)
{
return arg;
}

int main()
{
auto ret1 = func(520);
cout << "return value-1: " << ret1 << endl;

auto ret2 = func<double>(52.134);
cout << "return value-2: " << ret2 << endl;

auto ret3 = func<int>(52.134);
cout << "return value-3: " << ret3 << endl;

auto ret4 = func<char, int>(100);
cout << "return value-4: " << ret4 << endl;

return 0;
}

测试代码输出的结果为:

1
2
3
4
return value-1: 520
return value-2: 52.134
return value-3: 52
return value-4: d

根据得到的日志输出,分析一下示例代码中调用的模板函数:

  • auto ret = func(520);
    • 函数返回值类型使用了默认的模板参数,函数的参数类型是自动推导出来的为int类型。
  • auto ret1 = func<double>(52.134);
    • 函数的返回值指定为double类型,函数参数是通过实参推导出来的,为double类型
  • auto ret3 = func<int>(52.134);
    • 函数的返回值指定为int类型,函数参数是通过实参推导出来的,为double类型
  • auto ret4 = func<char, int>(100);
    • 函数的参数为指定为int类型,函数返回值指定为char类型,不需要推导

当默认模板参数和模板参数自动推导同时使用时(优先级从高到低):

  • 如果可以推导出参数类型则使用推导出的类型
  • 如果函数模板无法推导出参数类型,那么编译器会使用默认模板参数
  • 如果无法推导出模板参数类型并且没有设置默认模板参数,编译器就会报错。

看一下下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <string>
using namespace std;

// 函数模板定义
template <typename T, typename U = char>
void func(T arg1 = 100, U arg2 = 100)
{
cout << "arg1: " << arg1 << ", arg2: " << arg2 << endl;
}

int main()
{
// 模板函数调用
func('a');
func(97, 'a');
// func(); //编译报错
return 0;
}

程序输出的结果为:

1
2
arg1: a, arg2: d
arg1: 97, arg2: a

分析一下调用的模板函数func()

  • func('a'):参数T被自动推导为char类型,U使用的默认模板参数为char类型
  • func(97, 'a');:参数T被自动推导为int类型,U使用推导出的类型为char
  • func();:参数T没有指定默认模板类型,并且无法自动推导,编译器会直接报错
    • 模板参数类型的自动推导是根据模板函数调用时指定的实参进行推断的,没有实参则无法推导
    • 模板参数类型的自动推导不会参考函数模板中指定的默认参数。