C++模板与泛型编程:模板实参推断 (类型转换与模板类型实参、函数模板显式实参、尾置返回类型与类型转换、函数指针和实参推断)

模板实参推断

​ 我们知道,对于函数模板,编译器利用调用中的函数实参来确定其模板参数。从函数实参来确定模板实参的过程被称为模板实参推断。在模板实参推断过程中,编译器使用函数调用中的实参类型来寻找模板实参,用这些模板实参生成的函数版本与给定的函数调用最为匹配。

类型转换与模板类型实参

​ 在一次调用中传递给函数模板的实参被用来初始化函数的形参。当函数参数是模板类型参数时,它会采用特殊的初始化规则。只有很有限的几种类型转换会自动应用于这些实参。编译器通常不是对实参进行类型转换,而是生成一个新的模板实例

​ 同样,顶层 const 无论在形参还是实参,都会被忽略。在其他类型转换中,能在调用中应用于函数模板的包括以下两项:

  • const 转换:可以将一个非 const 对象的引用 (或指针) 传递给一个 const 引用 (或指针) 形参。
  • 数组或函数指针的转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以

其他类型转换,如算术转换、派生类向基类的转换以及用户定义的转换,都不能用于函数模板。

​ 下面是一个例子:

template <typename T> T fobj(T,T);		// 实参拷贝
template <typename T> T fref(const T&,const T&);	// 引用

string s1("a value");
const string s2("another val");
fobj(s1,s2);		// 调用 fobj(string,string),实参顶层 const 被忽略
fref(s1,s2);		// 调用 fref(const string&,const string&),形参顶层 const 被忽略

int a[10],b[52];
fobj(a,b);			// 调用 fobj(int*,int*);
fref(a,b);			// 错误

在 fobj 中,数组会自动转换为指针。但是在 fref 中,因为模板形参是引用,所以最后一个调用是错误的。

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

一个模板类型参数可以用作多个函数形参的类型。但由于只允许两种类型的类型转换,因此传递给这些形参的实参类型必须相同。如果推断出的类型不匹配,则调用就是错误的。

​ 如我们有:

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

当我们的调用方式为:

long lng;
compare(lng,1024);

这是错误的,因为 lng 是 long 类型,是 1024 是 int 类型。该调用无法实例化 compare 函数。

如果想要允许对函数实参的类型转换,我们可以将函数模板定义为两个类型参数

template <typename T,typename E> int compare(const T&,const E&);

这样,便可以提供不同类型的实参。如上述调用,便合法。

正常类型转换应用于普通函数实参

​ 函数模板可以有普通类型定义的参数,即不涉及模板类型参数的类型。这种函数实参不进行特殊处理;它们正常转换为对应形参的类型。如下函数模板:

template <typename T> ostream &print(ostream &os,const T &obj) {
    return os << obj;
}

我们可以看见,该函数模板的第一个参数是 ostream&,所以当我们调用此函数时,传递给它的实参会进行正常的类型转换:

print(cout, 42);		// 实例化 print(ostream&, int);
ofstream f("output");
print(f,10);			// 使用 print(ostream&, int); f 转换为 ostream&
函数模板显式实参

​ 在某些情况下,编译器无法推断出模板实参的类型。在其他一些情况下,我们希望允许用户控制模板实例化。当函数返回类型与参数列表中任何类型都不相同时,这两种情况最常出现。

指定显式模板实参

​ 作为一个允许用户指定使用类型的例子,我们将定义一个名为 sum 的函数模板,它接受两个不同类型的参数。我们希望允许用户指定结果的类型。这样,用户就可以选择合适的精度。

​ 我们可以定义表示返回类型的第三个模板参数,从而允许用户控制返回类型:

// 编译器无法推断 T1,它未出现在函数参数列表中
template <typename T1,typename T2,typename T3>
T1 sum(T2,T3);

编译器无法推断 T1,它未出现在函数参数列表中。所以每次调用 sum 时调用者必须为 T1 提供一个显式模板实参

​ 提供显式模板实参的方式与定义类模板实例的方式相同。在函数名之后,实参列表之前用尖括号指明

auto val3 = sum<long long>(i, lng);		// long long sum(int,long);

显式模板实参按由左至右的顺序与对应的模板参数匹配,即第一个模板实参与第一个模板参数匹配 … 依此类推。只有尾部 (最后) 参数的显式模板实参才可以忽略,前提是它们可以从函数参数推断出来

​ 如我们的 sum 函数如下编写:

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

我们总是必须提供三个形参指定实参:

auto val3 = alternative_sum<long long>(i, lng);		// 错误,T3 的类型未知
auto val2 = alternative_sum<long long,int,long>(i, lng);	// 正确,显式指定了三个参数
正常类型转换应用于显式指定的实参

​ 对于用普通类型定义的函数参数,允许进行正常的类型转换,出于同样的原因,对于模板类型参数已经显式指定了的函数参数,也进行正常的类型转换

// template <typename T> int compare(const T&,const T&);
long lng;
compare(lng,1024);			// 错误,模板参数不匹配
compare<long>(lng,1024);	// 正确,实例化 compare(long,long)
compare<int>(lng,1024);		// 正确,实例化 compare(int,int)
尾置返回类型与类型转换

​ 当我们希望用户确定返回类型时,用显式模板实参表示模板函数的返回类型时很有效的。但在其他情况下,要求显式指定模板实参会给用户增添额外负担。例如,我们可能希望编写一个函数,接受表示序列的一对迭代器和返回序列中一个元素的引用:

template <typename It>
??? &fcn(It beg,It end) {
    // 处理序列
    return *beg;
}

我们不知道返回结果的精确类型,但知道所需类型是所处理的序列的元素类型:

vector<int> vi = {1,2,3,4,5};
Blob<string> ca = {"hi","bye"};
auto &i = fcn(vi.begin(),vi.end()); // fcn 应该返回 int&
auto &j = fcn(ca.begin(),ca.end()); // fcn 应该返回 string&

我们知道,函数返回 *beg,而我们能够通过 decltype(*beg) 来获取此表达式的类型。但是,在编译器遇到函数的参数列表之前,beg 都是不存在的。所以,为了定义此类型,我们必须使用尾置返回类型:

template <typename It>
auto fcn(It beg,It end) -> decltype(*beg) {
    // 处理序列
    return *beg;		// 返回序列中一个元素的引用
}
进行类型转换的标准库模板类

​ 有时我们无法直接获得所需要的类型。例如,我们可能希望编写一个类似 fcn 的函数,但返回一个元素的值而非引用。

​ 在编写这个函数的过程中,我们面临一个问题:对于传递的参数的类型,我们几乎一无所知。在此函数中,我们知道唯一可以使用的操作是迭代器操作,而所有迭代器操作都不会生成元素,只能生成元素的引用。

​ 为了获得元素类型,我们可以使用标准库的类型转换模板。这些模板定义在 type_traits 中。我们可以用个 remove_references 来获得元素类型。此模板有一个模板类型参数和名为 type 的 public 类型成员。我们用给一个引用类型实例化 remove_reference,type 将表示被引用的类型。如:remove_references<int&>,其 type 为 int。

​ 有了这个,我们便可以这样编写函数:

template <typename It>
auto fcn(It beg,It end) -> remove_reference<decltype(*beg)>::type {
    // 处理序列
    return *beg;	// 返回序列中一个元素的拷贝
}

标准类型转换模板还有很多,见 p606

函数指针和实参推断

​ 当我们用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参 (对于函数指针来说,其类型还包括形参以及返回类型)。

​ 例如,假定我们有一个函数指针,它指向的函数返回 int,接受两个实参,每个参数都是指向 const int 的引用。我们可以使用该指针指向 compare 的一个实例:

template <typename T> int compare(const T&,const T&);
// pf1 指向实例 int compare(const int&,const int&);
int (*pf1)(const int&,const int&) = compare;

pf1 中参数的类型决定了 T 的模板实参的类型。在这里,T 的模板实参类型为 int。指针 pf1 指向 compare 的 int 版本实例。如果不能从函数指针类型确定模板实参,则产生错误:

void func(int(*)(const string&,const string&));
void func(int(*)(const int&,const int&));
func(compare);			// 错误,使用 compare 的哪个实例?

在这里,func 能接受 string 与 int 的实例,所以无法确定 func 的实参的唯一实例化版本,此调用失败。

​ 我们可以显式模板实参来消除这个歧义:

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

当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值