C++ 模板与泛型编程 《C++Primer》第16章(中上)———— 读书笔记

1、类型转换 与 模板类型参数

和普通的实参类似,传递给模板类型参数的 实参也能发生类型转换,但是只能发生两种类型转换:

  • const转换:可把非const对象的指针或引用,传递给一个const指针或引用 形参
  • 数组和函数名对应指针的转换:前提是函数形参不是引用类型,才可以对数组或函数类型的实参应用指针转换。数组实参可转换为指向其首元素的指针,函数实参可转换为该函数类型的指针。

只这两种转换,别无其它。
说的就是,算术转换、子类向父类的转换、用户自定义的转换,统统不能用于函数模板传参。
举个例子:

template<typename T> T func1(T, T);
template<typename T> T func2(const T&, const T&);

string s1("hello");
const string s2("world");
func1(s1, s2);		正确,实例化的是 func1(string, string); s2的const被忽略,你问为什么?
					func1 中实参被拷贝,你是不是const,关我拷贝之后的副本什么事?
func2(s1, s2);		正确,实例化的是 func2(const string&, cont string&); 
					s1 可以转换成 const

int a1[10], a2[42];
func1(a1, a2);		正确,实例化的是 func1(int*, int*);
func2(a1, a2);		错误,形参是引用,数组没法转换成指针

一句话总结:
在这里插入图片描述

使用 相同模板参数类型的 函数形参

若函数模板中 形参的参数类型相同,那么传入的实参类型也必须相同。
就比如这个函数模板:

template<typename T> void func1(T, T);		两个形参都是同一个模板参数类型
											所以传入的实参也应该一样
long l = 10;
short s = 1;
func1(l, 42);		错误,传入的类型要一样,这是一个 long 和 一个 int
func1(s, 42);		错误,同理

相传如不同类型的实参的话,只能再定义一个模板类型参数:

template<typename T, typename F> int func2(T x, F y)	实参类型可以不同,但必须兼容
{
	if (x < y) return 1;
	if (y < x) return 1;
	return 0;
}
func2(l, 42);		这回就对了
func2(s, 42);		正确
func2(42, 42);		传相同类型进去也是可以的

但要注意,上面的模板虽允许实参类型不同, 必须兼容
甚么意思?上面的func2函数中用到了<运算符,比较两个不同类型的对象,你自己必须定义了能比较这些类型的值的 <运算符
这里只是举了<的例子,实际上,函数模板中可能会利用传入的参数进行很多操作,-> . *等,太多了,传入的参数都必须确实能够进行这些操作。否则就是非法的。

正常的类型转换仍能用于普通函数参数

虽说对于模板类型参数的形参,只能应用两种类型转换,但是对于函数模板中的普通参数,仍然能使用各种类型转换。
在这里插入图片描述

例题一:
C++ 模板与泛型编程 《C++Primer》第16章(中上)———— 读书笔记_第1张图片
例题二:
C++ 模板与泛型编程 《C++Primer》第16章(中上)———— 读书笔记_第2张图片
const char*不会转换成string
例题三:
C++ 模板与泛型编程 《C++Primer》第16章(中上)———— 读书笔记_第3张图片
只有(e)的调用不合法。
把函数模板修改为下面这样才合法:

template<typename T> void f1(const T*, const T*);

这里把函数模板里的形参都改为了指针类型,这才符合《C++Primer》里的:
在这里插入图片描述
你原来的形参什么修饰都没有,既不是const的,也不是指针或引用,当然不行了。

2、函数模板显式实参

像类模板一样,我们也可以在使用函数模板的时候显式指定实参,这样可以实现自己控制返回值类型等操作。

2.1 如何指定显式模板实参

下面将定义一个名为sum的函数模板,他接收两个不同类型的参数。我们希望用户指定结果的类型,这样,就能灵活的控制结果的精度。我们定义表示返回类型的第三个参数,从而允许用户控制返回类型:

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

不难发现,这个函数模板没有任何参数的类型可以用来推断T1的类型。因为我们显式提供实参的位置与定义类模板实例相同:都在模板名字后面的尖括号内。

int x = 10;
double y = 3.14;
auto result = sum<int>(x, y);		实例化了 int sum(int, double); 

此调用显式指定T1的类型,而T2 T3的类型由编译器推断出来。
注:每次调用sum这样的函数模板,都必须T1提供显式模板实参。因为你不提供,编译器就不知T1啥类型,不知道啥类型,编译器就报错。

2.2 显式模板实参的匹配顺序

如果有多个模板类型参数都需要我们显示提供,该怎么办?
在调用函数模板时,仍然是把所有的显式提供的实参写在尖括号里,和模板类型参数的匹配顺序是,从左到右,一对一匹配。(第一个显式模板实参与第一个模板类型参数匹配)

有这样一个模板
template<typename T1, typename T2, typename T3> pair<T1, T2> func(T3);

auto twin = func<double, long>func(3.14);	实例化了 pair<double, long> func(double);
cout << typeid(twin).name() << endl;

输出结果:

struct std::pair<double, long>

再来看下面的一个糟糕的设计,你千万不要学:

template<typename T1, typename T2, typename T3> T3 func(T2, T1);

auto x = func<int, int, double>(1, 2);

对于上面这个函数模板,我们必须为T3显式提供实参,T2 T1理论上来说可以有编译器推断出来。但是,由于显式实参是从左到右,一对一匹配的,导致我们不得不先给T1 T2提供显式实参,最后才能给T3提供,所以说这是一个糟糕的设计。

2.3 显式指定的实参 可以使用 正常类型转换

本篇博客的第一大点中,对类型转换的限制 是因为模板类型参数未知,但是如果我们提供了显式的实参,那么对应的函数参数就能进行正常的类型转换了:

有这样一个函数模板
template<typename T1, typename T2> void func(T1, T2);

short x = 1;
long long y = 10;
func<int, double>func(x, y);	func()的两个模板类型参数都显式指定好了,那就可以随便传参了啊
								只要能转化成 intdouble 就行

3、尾置返回类型 与类型转换

3.1 尾置返回值类型 在函数模板返回值中的妙用

当希望用户确定返回值类型时,用显式模板实参表示模板函数的返回值类型确实有效。但在某些情况下,显式指定模板实参会很不方便,而且并不会带来好处。
例如,要编写函数模板,接收一对迭代器,处理序列中的元素,并返回序列中某个元素的引用。我们并不知道返回值的准确类型,只知道他是所处理序列的元素类型。

该函数模板大致形如:
template<typename It> ??? func(It beg, It end);		

在上面不完整的函数模板中,我们可以知道,返回值类型其实就是*beg的类型,但是,指定函数返回值类型的位置在beg出现的位置(参数列表)之前,所以此时,我们想到使用尾置返回值类型。

template<typename It> auto func(It beg, It end) -> decltype(*beg);

尾置返回值类型完美的解决了问题。解引用运算符返回一个左值,所以,通过decltype推断出的类型为beg指向元素的类型的引用。

3.2 进行类型转换的标准库模板类

有时,我们无法直接获得所需要的类型。例如,我们可能希望编写一个类似上面func的函数,但返回元素值的拷贝而非引用
在编写这个函数的过程中,我们面临一个问题:对于传递的参数的类型,我们一无所知。在此函数中,唯一可以使用的操作是迭代器操作,而所有迭代器操作都不会生成元素,只能生成元素的引用,但我们想要的是值,必须要知道元素的类型。

为此,我们可以使用标准库的类型转换模板(下表所示)。
C++ 模板与泛型编程 《C++Primer》第16章(中上)———— 读书笔记_第4张图片

本例中,使用remove_reference获得元素类型。remove_reference模板有一个模板类型参数和一个名为type的(public)类型成员。如果用一个引用类型实例化remove_reference,其type成员将表示被引用的类型
例如,给定迭代器beg

remove_reference<decltype(*beg)>::type		*beg 返回元素类型的引用

可通过上面的语句获得元素的类型。组合使用 尾置返回值类型 和 类型转换模板,就可以在函数中返回元素值的拷贝。新的func模板如下:

template<typename It> auto func(It beg, It end) -> typename remove_reference<decltype(*beg)>::type
{
	// ......
}

返回值里为什么必须加typename?《C++Primer》:
type是一个类的成员,而该类依赖于一个模板参数。因此,必须在返回值类型的声明中使用typename告知编译器,type表示一个类型。

我自己的详解:
typename告诉编译器后面表示的是一个类型,必须加typename,说明编译器自己不知道type表示的是类型。为什么呢?
那就先看::前面,::前面是编译器可知的,那后面的type就也是可知的。从最里面看,beg是模板类型参数,编译器自然不知道是什么类型,那编译器自然也不知道decltype的结果是什么类型,那么remove_reference的类型就更不知道了。
综上,编译器无法得知::前面的类到底定义了什么,怎么可能知道里面的type表示的是一个类型,所以我们要加上typename告诉编译器,type是一个类型。

说回类型转换模板,表 16.1 中描述的每个类型转换模板的工作方式都与remove_reference类似。每个模板都有一个名为typepublic成员,表示一个类型。此类型与模板自身的模板类型参数相关,其关系看模板名就知道。
如果不可能(或不必要)转换模板参数,则type成员就是模板参数类型本身。
例如,若T是一个指针类型,则remove_pointer::typeT指向的类型。如果T不是一个指针类型,则无需进行任何转换,因此,type具有与T相同的类型。

4、函数指针 和模板

编译器可以使用函数指针的类型 推断模板实参。

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

上面的例子中,p指向了compareint版本实例。

如果不能从函数指针类型确定模板实参,则产生错误:

void func( int (*)(const double&, const double&) );		接收函数指针
void func( int (*)(const int&, const int&) );			重载版本,接受不同类型的函数指针
func(compare);		错误,不知道使用 compare 哪个版本的实例

func(compare<int>);	正确,传递 compare(const int&, const int&);

在这里插入图片描述

5、模板实参推断 和 引用

5.1 从 值引用函数参数 推断T的类型

当函数参数是模板类型参数的普通左值引用(T&)时,只能传递给他一个左值,如,一个变量或一个返回引用类型的表达式。实参可以是const的,也可以不是,若为const的,则T的类型被推断为const类型:

template<typename T> void func1(T&);
int i;
const int ci;
func1(i);		T 为 int
func1(ci);		T 为 const int
func(5);		错误,5 是右值

当函数参数类型是const T&,我们可以传递给他任何类型的实参,一个对象(const或非const),一个临时对象,或一个字面值常量。当函数参数 本身是const 时,T的类型推断结果不会是const类型,因为const已经是函数参数类型的一部分了,不会再是模板参数类型的一部分了。

template<typename T> void func2(const T&);
int i;
const int ci;
func2(i);		T 为 int
func2(ci);		T 仍然为 int
func(5);		这回就对了,const T& 可以绑定到右值,T 为 int

5.2 从 值引用函数参数 推断T的类型

当函数参数是个右值引用(T&&)时,推断过程类似普通左值引用函数参数的推断过程:

template<typename T> void func3(T&&);
func3(42);		T 为 int

5.3 引用折叠 和 右值引用参数

假定i是一个int对象,你可能认为像func3(i)这样的调用是不合法的。毕竟,i是一个左值,而通常我们不能将一个右值引用绑定到一个左值上。但是,C++语言在正常绑定规则之外定义了两个例外规则,允许这种绑定。这两个例外规则是move这种标准库设施正确工作的基础。

第一个例外规则影响右值引用参数的推断如何进行。

当把一个左值传给函数的右值引用参数,且此右值引用绑定到模板类型参数,也就是T&&,编译器推断模板类型参数为传入实参的左值引用类型。因此,当调用func3(i)时,编译器推断T的类型为int&,而不是int

T被推断为int&看起来好像意味着func3的函数参数应该是int&类型的右值引用类型,也就是int& &&。但是,通常,不能定义引用的引用。不过,可以通过类型别名,或 模板类型参数间接定义。

这时,就有了第二种例外规则。

第二个例外规则:引用折叠

如果间接创建一个引用的引用,则这些引用会形成【折叠】。大部分情况下,引用会折叠成普通的左值引用类型,只有一种情况下,会折叠成右值引用。如下所示:

  • T& &, T& &&, T&& &这三种都会折叠成类型 T&
  • 只有T&& &&会折叠成T&&

引用折叠只能用于间接创建的引用的引用,如类型别名 或 模板参数。

加入了引用折叠规则,就能对一个左值调用func3了,当把一个左值传递给func3的右值引用函数参数时,编译器推断T为一个左值引用类型,而函数参数类型就折叠成了int&

func3(i);	实参是左值,T 为 int&,实例化了 void func3<int&>(int&);
func3(ci);	实参是左值,T 为 const int&,实例化了 void func3<const int&>(const int&);
两个例外规则 导致的结果:
  • ① 若函数参数是绑定到模板类型参数的右值引用(如,T&&),则可以往函数中传入左值;且
  • ②如果实参就是一个左值,则推断出的模板类型参数将是一个左值引用,且函数参数被实例化为一个左值引用参数。

此外,这两个规则还暗示,可以把任意类型的实参传递给T&&类型的函数参数。

5.4 编写 接受右值引用参数的模板函数

模板类型参数可以推断为一个引用类型,这一特性对模板内的代码有巨大的影响:

template<typename T> void func3(T&& val)  不清楚 T 到底是什么类型,intint&?反正不会是int&&
{
	T t = val;			是拷贝?还是把 val 绑定到 t
	t = fcn(t);			赋值只改变了t,还是 val 也变了
	if (val == t)		若 T 为引用,则必是 true
	{ 
		// ....... //
	}
}

当对一个右值调用func3时,如func3(42)Tint
当对一个左值调用func3时,如func3(i)Tint&
C++ 模板与泛型编程 《C++Primer》第16章(中上)———— 读书笔记_第5张图片

6、深入理解std::move

6.1 std::move是如何定义的?

move其实也是一个函数模板,毕竟她能接受任何类型的实参,只有模板才能做到。其具体定义如下:

template<typename T> typename remove_reference<T>::type&& move(T&& t)
{
	return static_cast<typename remove_reference<T>::type&&>(t);
}

这段代码很短但很精妙。
move的函数参数T&&是一个绑定到模板类型参数的右值引用。通过引用折叠,此参数可以与任何类型的实参匹配。也就是:既可以传入左值,也可以传入右值。

6.2 std::move是如何工作的?

思考如下两条赋值语句:

string s1("hi!"), s2;
s2 = std::move(string("bey!"));
s2 = std::move(s1);
第一个赋值

第一个赋值中,传给move的实参是一个临时构造的匿名string对象,很显然是个右值,上小节说过,当向一个右值引用函数参数传递一个右值时,由实参推断出的类型为被引用的类型。所以有:

  • 推断出 Tstring
  • 所以,remove_reference使用string实例化
  • remove_referencetype成员是string(本来就是string,所以remove_reference什么都不做)
  • 所以,move的返回类型为string&&,且
  • move的函数参数t的类型为string&&

因此,这个调用实例化了:

string&& move(string&& t);

函数体中,返回的是static_cast(t),使用static_castt现实的转换成string&&。因此,此调用的结果就是他所接受的右值引用,且该右值引用的确是一个右值,因为他没有名字。
(注意:这里的t并不是右值,虽然他是string&&类型,但他有名字,也就是t,所以是个左值。)

第二个赋值

在此调用中,传给move的实参是一个左值,所以有:

  • 推断出的T的类型为 string&
  • 所以,remove_referencestring&实例化
  • remove_referencetype成员是string
  • move的返回类型是string&&
  • move的函数参数t实例化为string& &&,会折叠为string&

综上,该调用实例化了:

string&& move(string& t);

函数体中,返回的是static_cast(t),这回,t的类型为stirng&cast将其转换为string&&

6.3 左值转换为右值引用是static_cast的特权

不能隐式的把左值转换为右值引用,但可以用static_cast显式地将左值转换为右值引用。

对于操作右值引用的代码来说,把右值引用绑定到左值的特性允许他们截断左值

你可能感兴趣的:(C++,c++)