章节目录

第11章总结

本节阅读量:

函数模板可能看起来相当复杂,但它们是一种非常强大的方法,可以使代码与不同类型的对象一起工作。在以后的章节中,我们将看到更多的模板内容。


章节回顾

函数重载允许我们创建具有相同名称的多个函数,只要每个同名函数具有不同的参数类型集(或者可以以其他方式区分函数)。这样的函数称为重载函数(简称重载)。返回类型不在区分范围内。

在解析重载函数时,如果找不到精确匹配,编译器将更喜欢可以通过数值提升匹配的重载函数,而不是那些需要数值转换的重载函数。当对已重载的函数进行函数调用时,编译器将基于函数调用中使用的参数,尝试将函数调用与适当的重载相匹配。这称为重载决议。

当编译器发现两个或多个函数可以与重载函数的函数调用相匹配,但无法确定哪一个是最佳的时,就会发生不明确的匹配。

默认参数是为函数参数提供的默认值。具有默认参数的参数必须始终是最右边的参数,并且在解析重载函数时,它们不用于区分函数。

函数模板允许我们创建类似函数的定义,该定义用作创建相关函数的模板。在函数模板中,我们使用模板类型参数作为以后要指定的任何类型的占位符。告诉编译器我们正在定义模板并声明模板类型的语法称为模板参数声明。

从函数模板(具有模板类型)创建函数(具有特定类型)的过程简称为函数模板实例化(或实例化)。当这个过程由于函数调用而发生时,它被称为隐式实例化。实例化的函数称为函数实例(或简称为实例,有时称为模板函数)。

模板参数推导允许编译器从函数调用的参数中推导出应用于实例化函数的实际类型。模板参数推导不进行类型转换。

模板类型有时称为泛型类型,使用模板进行编程有时称为泛型编程。

在C++20中,当auto关键字用作普通函数中的参数类型时,编译器会自动将函数转换为函数模板,每个auto参数都成为独立的模板类型参数。这种创建函数模板的方法称为简写函数模板。

非类型模板参数是具有固定类型的模板参数,它作为模板参数传入的constexpr值的占位符。


习题

问题 1

1a) 下面这个程序的输出是什么?为什么?

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

void print(int x)
{
    std::cout << "int " << x << '\n';
}

void print(double x)
{
    std::cout << "double " << x << '\n';
}

int main()
{
    short s { 5 };
    print(s);

    return 0;
}
查看答案

输出是int 5。把short转换为int属于数值提升,而把short转换为double属于数值转换。在数值提升和数值转换之间,编译器会优先选择数值提升。

1b) 下面这段代码为什么无法通过编译?

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

void print()
{
    std::cout << "void\n";
}

void print(int x=0)
{
    std::cout << "int " << x << '\n';
}

void print(double x)
{
    std::cout << "double " << x << '\n';
}

int main()
{
    print(5.0f);
    print();

    return 0;
}
查看答案

因为带有默认实参的形参不参与重载函数的匹配判定,编译器无法判断对print()的调用应当匹配print()还是print(int x=0)。

1c) 下面这段代码为什么无法通过编译?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>

void print(long x)
{
    std::cout << "long " << x << '\n';
}

void print(double x)
{
    std::cout << "double " << x << '\n';
}

int main()
{
    print(5);

    return 0;
}
查看答案

字面值5是int。把int转换为long或转换为double都属于数值转换,编译器无法判断哪个函数的匹配更好。

问题 2

第1步

编写一个名为add()的函数模板,使用户能够把2个相同类型的值相加。下面这个程序应当能够运行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <iostream>

// 在这里编写你的 add 函数模板

int main()
{
	std::cout << add(2, 3) << '\n';
	std::cout << add(1.2, 3.4) << '\n';

	return 0;
}

并产生如下输出:

5
4.6
查看答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>

template <typename T>
T add(T x, T y)
{
	return x + y;
}

int main()
{
	std::cout << add(2, 3) << '\n';
	std::cout << add(1.2, 3.4) << '\n';

	return 0;
}

第2步

编写一个名为mult()的函数模板,使用户能够把一个任意类型的值(第一个形参)与一个整数(第二个形参)相乘。第二个形参不应是模板类型。函数应当返回与第一个形参相同的类型。下面这个程序应当能够运行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <iostream>

// 在这里编写你的 mult 函数模板

int main()
{
	std::cout << mult(2, 3) << '\n';
	std::cout << mult(1.2, 3) << '\n';

	return 0;
}

并产生如下输出:

6
3.6
查看答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>

template <typename T>
T mult(T x, int y)
{
	return x * y;
}

int main()
{
	std::cout << mult(2, 3) << '\n';
	std::cout << mult(1.2, 3) << '\n';

	return 0;
}

第3步

编写一个名为sub()的函数模板,使用户能够把两个不同类型的值相减。下面这个程序应当能够运行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <iostream>

// 在这里编写你的 sub 函数模板

int main()
{
	std::cout << sub(3, 2) << '\n';
	std::cout << sub(3.5, 2) << '\n';
	std::cout << sub(4, 1.5) << '\n';

	return 0;
}

并产生如下输出:

1
1.5
2.5
查看答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

template <typename T, typename U>
auto sub(T x, U y)
{
	return x - y;
}

/*
//如果支持 C++20,可以使用简写函数模板代替
auto sub(auto x, auto y)
{
	return x - y;
}
*/

int main()
{
	std::cout << sub(3, 2) << '\n';
	std::cout << sub(3.5, 2) << '\n';
	std::cout << sub(4, 1.5) << '\n';

	return 0;
}

问题 3

下面这个程序的输出是什么?为什么?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>

template <typename T>
int count(T) // 等价于 int count(T x),只是形参没有被使用,所以不给它命名
{
    static int c { 0 };
    return ++c;
}

int main()
{
    std::cout << count(1) << '\n';
    std::cout << count(1) << '\n';
    std::cout << count(2.3) << '\n';
    std::cout << count<double>(1) << '\n';

    return 0;
}
查看答案

输出为:

1
2
1
2

第一次调用count(1)时,编译器会实例化函数count<int>(int)并调用它,返回值为1。

再次调用count(1)时,编译器发现count<int>(int)已经存在,于是再次调用它,返回值为2。

调用count(2.3)时,编译器会实例化原型为count<double>(double)的函数并调用它。这是一个新函数,有自己独立的静态变量c,因此返回值为1。

调用count<double>(1)时,编译器发现我们显式要求使用count()的double版本。这个函数由于上一条语句已经存在,因此会调用count<double>(double),而int实参会隐式转换为double。这个函数会返回2。

问题 4

下面这个程序的输出是什么?

 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
#include <iostream>

int foo(int n)
{
    return n + 10;
}

template <typename T>
int foo(T n)
{
    return n;
}

int main()
{
    std::cout << foo(1) << '\n'; // #1

    short s { 2 };
    std::cout << foo(s) << '\n'; // #2

    std::cout << foo<int>(4) << '\n'; // #3

    std::cout << foo<int>(s) << '\n'; // #4

    std::cout << foo<>(6) << '\n'; // #5

    return 0;
}
查看答案

输出为:

11
2
4
2
6

第1种情况:foo(1)与foo(int)精确匹配,因此调用非模板函数foo(int)。

第2种情况:foo(s)与非模板函数foo(int)并不精确匹配,但实参s可以转换为int,因此foo(int)是候选函数。然而,编译器更倾向于使用函数模板foo<T>(T)来「刻印」出精确匹配的foo<short>(short)。因此这里调用的是foo<short>(short)。

第3种情况:foo<int>(4)是对foo<int>的显式调用,因此不考虑foo(int)。编译器刻印出foo<int>(int)并调用它。

第4种情况:这同样是对foo<int>的显式调用。编译器把实参s提升为int以匹配形参。

第5种情况:这种语法只会匹配函数模板,因此不考虑foo(int)。这里调用foo<int>(int)。


11.8 非类型模板参数

上一节

12.0 复合数据类型简介

下一节