和普通的实参类似,传递给模板类型参数的 实参也能发生类型转换,但是只能发生两种类型转换:
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
函数中用到了<
运算符,比较两个不同类型的对象,你自己必须定义了能比较这些类型的值的 <
运算符。
这里只是举了<
的例子,实际上,函数模板中可能会利用传入的参数进行很多操作,->
.
*
等,太多了,传入的参数都必须确实能够进行这些操作。否则就是非法的。
虽说对于模板类型参数的形参,只能应用两种类型转换,但是对于函数模板中的普通参数,仍然能使用各种类型转换。
例题一:
例题二:
const char*
不会转换成string
例题三:
只有(e)的调用不合法。
把函数模板修改为下面这样才合法:
template<typename T> void f1(const T*, const T*);
这里把函数模板里的形参都改为了指针类型,这才符合《C++Primer》里的:
你原来的形参什么修饰都没有,既不是const
的,也不是指针或引用,当然不行了。
像类模板一样,我们也可以在使用函数模板的时候显式指定实参,这样可以实现自己控制返回值类型等操作。
下面将定义一个名为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
啥类型,不知道啥类型,编译器就报错。
如果有多个模板类型参数都需要我们显示提供,该怎么办?
在调用函数模板时,仍然是把所有的显式提供的实参写在尖括号里,和模板类型参数的匹配顺序是,从左到右,一对一匹配。(第一个显式模板实参与第一个模板类型参数匹配)
有这样一个模板
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
提供,所以说这是一个糟糕的设计。
本篇博客的第一大点中,对类型转换的限制 是因为模板类型参数未知,但是如果我们提供了显式的实参,那么对应的函数参数就能进行正常的类型转换了:
有这样一个函数模板
template<typename T1, typename T2> void func(T1, T2);
short x = 1;
long long y = 10;
func<int, double>func(x, y); func()的两个模板类型参数都显式指定好了,那就可以随便传参了啊
只要能转化成 int 和 double 就行
当希望用户确定返回值类型时,用显式模板实参表示模板函数的返回值类型确实有效。但在某些情况下,显式指定模板实参会很不方便,而且并不会带来好处。
例如,要编写函数模板,接收一对迭代器,处理序列中的元素,并返回序列中某个元素的引用。我们并不知道返回值的准确类型,只知道他是所处理序列的元素类型。
该函数模板大致形如:
template<typename It> ??? func(It beg, It end);
在上面不完整的函数模板中,我们可以知道,返回值类型其实就是*beg
的类型,但是,指定函数返回值类型的位置在beg
出现的位置(参数列表)之前,所以此时,我们想到使用尾置返回值类型。
template<typename It> auto func(It beg, It end) -> decltype(*beg);
尾置返回值类型完美的解决了问题。解引用运算符返回一个左值,所以,通过decltype
推断出的类型为beg
指向元素的类型的引用。
有时,我们无法直接获得所需要的类型。例如,我们可能希望编写一个类似上面func
的函数,但返回元素值的拷贝而非引用。
在编写这个函数的过程中,我们面临一个问题:对于传递的参数的类型,我们一无所知。在此函数中,唯一可以使用的操作是迭代器操作,而所有迭代器操作都不会生成元素,只能生成元素的引用,但我们想要的是值,必须要知道元素的类型。
本例中,使用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
类似。每个模板都有一个名为type
的public
成员,表示一个类型。此类型与模板自身的模板类型参数相关,其关系看模板名就知道。
如果不可能(或不必要)转换模板参数,则type
成员就是模板参数类型本身。
例如,若T
是一个指针类型,则remove_pointer
是T
指向的类型。如果T
不是一个指针类型,则无需进行任何转换,因此,type
具有与T
相同的类型。
编译器可以使用函数指针的类型 推断模板实参。
template<typename T> int compare(const T&, const T&);
int (*p)(const int&, const int&) = compare;
上面的例子中,p
指向了compare
的int
版本实例。
如果不能从函数指针类型确定模板实参,则产生错误:
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&);
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
T
的类型当函数参数是个右值引用(T&&
)时,推断过程类似普通左值引用函数参数的推断过程:
template<typename T> void func3(T&&);
func3(42); T 为 int
假定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&&
类型的函数参数。
模板类型参数可以推断为一个引用类型,这一特性对模板内的代码有巨大的影响:
template<typename T> void func3(T&& val) 不清楚 T 到底是什么类型,int?int&?反正不会是int&&
{
T t = val; 是拷贝?还是把 val 绑定到 t
t = fcn(t); 赋值只改变了t,还是 val 也变了
if (val == t) 若 T 为引用,则必是 true
{
// ....... //
}
}
当对一个右值调用func3
时,如func3(42)
,T
为int
。
当对一个左值调用func3
时,如func3(i)
,T
为int&
。
std::move
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&&
是一个绑定到模板类型参数的右值引用。通过引用折叠,此参数可以与任何类型的实参匹配。也就是:既可以传入左值,也可以传入右值。
std::move
是如何工作的?思考如下两条赋值语句:
string s1("hi!"), s2;
s2 = std::move(string("bey!"));
s2 = std::move(s1);
第一个赋值中,传给move
的实参是一个临时构造的匿名string
对象,很显然是个右值,上小节说过,当向一个右值引用函数参数传递一个右值时,由实参推断出的类型为被引用的类型。所以有:
T
为string
remove_reference
使用string
实例化remove_reference
的type
成员是string
(本来就是string
,所以remove_reference
什么都不做)move
的返回类型为string&&
,且move
的函数参数t
的类型为string&&
因此,这个调用实例化了:
string&& move(string&& t);
函数体中,返回的是static_cast
,使用static_cast
把t
现实的转换成string&&
。因此,此调用的结果就是他所接受的右值引用,且该右值引用的确是一个右值,因为他没有名字。
(注意:这里的t
并不是右值,虽然他是string&&
类型,但他有名字,也就是t
,所以是个左值。)
在此调用中,传给move
的实参是一个左值,所以有:
T
的类型为 string&
remove_reference
用string&
实例化remove_reference
的type
成员是string
move
的返回类型是string&&
move
的函数参数t
实例化为string& &&
,会折叠为string&
综上,该调用实例化了:
string&& move(string& t);
函数体中,返回的是static_cast
,这回,t
的类型为stirng&
,cast
将其转换为string&&
。
static_cast
的特权不能隐式的把左值转换为右值引用,但可以用static_cast
显式地将左值转换为右值引用。
对于操作右值引用的代码来说,把右值引用绑定到左值的特性允许他们截断左值。