C++Primer_Chap16_模板和泛型编程_List02_模板实参推断_笔记

  从函数实参类确定模板实参的过程称为模板实参推断(template argument deduction)

类型转换和模板类型参数

  如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则。只有很有限的几种类型转换会自动应用于这些实参。编译器通常不是对实参进行类型转换,而是生成一个新的模板实例。能在调用中应用于函数模板的包括如下两项:

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

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

template <typename T> T fobj(T, T);    //实参被拷贝
template <typename T> T fref(const T&, const T&);    //引用
string s1("a value");
const string s2("another value");

fobj(s1, s2);    //调用fobj(string, string);const被忽略
fref(s1, s2);    //调用fref(cosnt string&, const string&)
                    //将s1转换成const是允许的

int a[10], b[42];
fobj(a, b);    //调用f(int*, int*)
fref(a, b);    //错误:数组类型不匹配

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

  一个模板类型参数可以用作多个函数形参的类型。由于只允许有限的几种类型转换,因此传递给这些形参的实参必须具有相同的类型。

long lng;
compare( lng, 1024);    //错误:不能实例化compare(long, int)

  如果希望允许对函数实参进行正常的类型转换,我们可以将函数模板定义为两个类型参数:

template <typename A, typename B>
int filexibleCompare( const A& v1, const B& v2)
{
    //……
}

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

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

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

print(cout, 43);    //实例化print(ostream&, int)

ofstream f("output");
print(f, 10);    //使用print(ostream&, int):将f转换成ostream&

函数模板显式实参

指定显式模板实参

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

//T1显式指定,T2和T3是从函数实参类型推断而来的
auto val3 = sum<long long>(i, lng);    //long long sum(int,long)

  显式模板实参按由左至右的顺序与对应的模板参数匹配:第一个模板参数与第一个模板参数匹配,第二个实参与第二个参数匹配,依次类推。只有尾部(最右)参数的显示模板实参才可以忽略:

//糟糕的设计:用于必须指定所有三个模板参数
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T2, T1);

//错误:不能推断前几个模板参数
auto val3 = alternative_sum<long long>(i, lng);

正常类型转换应用于显示指定的实参

  对于用普通类型定义的函数参数,允许进行正常的类型转换:

long lng;
compare<long>(lng, 1024);
compare<int>(lng, 1024);

尾置返回类型与类型转换

  当我们希望用户确定返回类型时,用显式模板实参表示模板函数的返回类型是很有效的。但在其他情况下,要求显式指定模板实参会给用户增添额外的负担且不会有什么好处。

template <typename T>
??? &fcn(T beg, T end)
{
    //……
    return *beg;
}

vector<int> vi = {1, 2, 3, 4, 5};
Blob<string> ca = {"hi", "bye"};
auto &i = fcn(vi.begin(), bi.end());
auto &s = fcn(ca.begin(), ca.end());

  在此例中,我们知道函数应该返回*beg,而且知道我们可以用decltype(*beg)来获取此表达式的类型。但是在编译器遇到函数的参数列表之前,beg都不存在。为了定义此函数,我们必须采用位置返回类型。

template <typename T>
auto fcn(T beg, T end)->decltype(*beg)
{
    //……
    return *beg;
}

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

  有时我们无法直接获取所需的类型。比如希望编写一个返回元素值而不是引用的类似fcn的函数。对于传递的参数的类型,我们几乎一无所知,唯一可以使用的操作符是迭代器操作,而所有迭代器操作都不会生成元素,只能生成元素的引用。为了获取元素类型,我们可以使用标准库的类型转换(type transformation)模板。这些模板定义在头文件中type_traits。这个头文件中的类通常用于所谓的模板元程序设计。

#include <type_traits>
remove_reference<decltype(*beg)>::type

  remove_reference::type脱去引用,剩下元素类型本身。我们必须在返回类型的什么中使用typename来告知

templare <typename T>
auto fcn2(T beg, T end)->typename remove_reference<decltype(*beg)>::type
{
    return *beg;
}
标准类型转换模板
对Mod<T>,其中Mod为若T为则Mod<T>::type为
remove_reference

X&或X&&

否则

X

T

add_const

X&、const  X或函数

否则

T

const T

add_lvalue_reference

X&

X&&

否则

T

X&

T&

add_rvalue_reference

X&或X&&

否则

T

T&&

remove_pointer

X*

否则

X

T

add_pinter

X&或X&&

否则

X*

T*

make_signed

unsigned X

否则

X

T

make_unsigned

带符号类型

否则

unsigned X

T

remove_extent

X[n]

否则

X

T

remove_all_extents

X[n1][n2]…

否则

X

T

函数指针和实参推断

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

//pf1指向实例int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;

//func的重载版本:每个版本接受一个不同的函数指针类型
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
//错误:二义性
func(compare);
//正确:显式指出实例化版本
fun(compare<int>);

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

模板实参推断和引用

template <typename T> void f(T &p);

  函数参数p是一个模板类型参数T的引用,非常重要的是记住两点:编译器会应用正常的引用绑定规则;const是底层的,不是顶层的。

从左值引用函数参数推断类型

  当一个函数参数是模板类型参数的一个普通(左值)引用时(T&),只能传递给它一个左值。实参如果是const的,则T被推断成const类型:

template <typename T> void f1(T&);

f1(i);    //i:int;  T:int
f1(ci);    //ci:const int;  T: const int 
f1(5);    //错误:传递给一个&参数的实参必须是左值

  如果一个函数的参数的类型是const T&,正常的绑定规则告诉我们可以传递给它任何类型的实参——一个对象(const或非const)、一个临时对象或是一个字面值常量。

template <typename T> void f2(const T&);

f2(i);    //i:int;  T:int
f2(ci);    //ci:const int;  T:  int 
f2(5);    //正确

引用折叠和右值引用参数

template <typename T> void f3(T&&);

  通常不能将一个右值引用绑定到一个左值上。但C++语言在正常绑定规则之外定义了两个例外规则,允许这种绑定。这两个例外规则是move这种标准库设施正确工作的基础:

  1. 第一个例外规则影响右值引用参数的推断如何进行。当将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数(T&&),编译器推断模板类型参数为实参的左值引用类型。通常,不能直接定义一个引用的引用,但通过类型别名或通过模板类型参数间接定义是可以的。
  2. 如果间接创建了一个引用的引用,则这些引用形成了“折叠”。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。只在一种特殊情况下引用会折叠成右值引用:右值引用的右值引用。即,给定一个类型X:
  • X& &,X& &&和X&& &都折叠成类型X&
  • X&& &&折叠成X&&

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

f3(i);    //实参是一个左值;模板参数T是int&
f3(ci);    //实参是一个左值;模板参数T是const int&

//用于演示的无效代码
void f3<int &>(int& &&);    //T是int&,函数参数是int& &&。折叠为int&

  如果一个函数参数是指向模板参数类型的右值引用(T&&),则可以传递给它任一类型(左值或右值)的实参。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用。

template <typename T> void f3(T&& val)
{
    T t = val;    //拷贝 OR 绑定一个引用
    t = fcn(t);    //赋值只改变t OR 又改变t又改变val
    if(val == t)    //若T为引用类型,一直为true
    {    
        /*    */
    }
}

当我们f3(42)时,T为int,局部变量t是int,通过拷贝参数val的值初始化。对t赋值,参数val保持不变

当我们f3(i)时,T为int&,定义并初始化局部变量t时,类型为int&。

  在实际中,右值引用通常用于两种情况:模板转发其实参或模板被重载。重载通常如下方式:

template <typename T> void f(T&&);    //绑定到非const右值
template <typename T> void f(const T&);    //左值和const右值

理解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&&是一个指向模板类型参数的右值引用。通过引用折叠,此参数可以与任何类型的实参匹配。

string s1("hi"), s2;
s2 = std::move(string("bye"));    //正确,从一个右值移动数据
s2 = std::move(s1);                //正常:但在赋值后,s1的值是不确定的

  在std::move(string("bye"))中:

  • 推断出T的类型是string
  • 因此,remove_reference用string实例化
  • remove_reference<string>::type是string
  • move返回类型是static_cast<string&&>(t),即string&&
  • move的函数参数t的类型是string&&
  • 所以这个调用实例化move<string>,即函数 string&& move(string &&t)

  在std::move(s1)中:

  • T的类型是string的引用
  • 因此,remove_reference用string&实例化
  • remove_reference<string&>::type是string
  • move返回类型是static_cast<string&&>(t),即string&&
  • move的函数参数t实例化为string& &&,这得为string&
  • 所以这个调用实例化move<string&>,即函数string&& move(string &t)

从一个左值static_cast到一个右值引用是允许的。通常情况,static_cast只能用于其他合法的类型转换。但是,这里又有一条针对右值引用的特许规则:虽然不能隐式的将一个左值转换成右值引用,但可以用static_cast显式的将一个左值转换成一个右值引用。

转发

  某些函数需要将其一个或多个实参连同类型不变的转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是const以及实参是左值还是右值:

//接受一个可调用对象和另外两个参数的模板
//对“翻转”的参数调用给定的可调用对象
//flip1是一个不完整的实现:顶层const和引用丢失了
template<typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
    f(t2, t1);
}

void f(int v1, int &v2)
{
    cout << v1 << " " << ++v2 << endl;
}

flip1( f, j, 42);    //通过flip1调用f不会改变j

  这个flip1函数在调用一个接受引用参数的函数时会出现问题。

定义能保持类型信息的函数参数

  如果一个函数参数是指向模板类型参数的右值引用,它对应的实参的const属性和左/右值属性将得到保持。

template<typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
    f(t2, t1);
}

void g(int &&v1, int &v2)
{
    cout << v1 << " " << ++v2 << endl;
}

flip( g, i, 42);    //错误:不能从一个左值实例化int&&

这个版本的flip2解决了一半问题,但不能用于接受右值引用参数的函数。在flip2中i是int&,到g中v1应该是int&&,类型转换出错。

在调用中使用std::forward保持类型信息

  我们可以使用一个名为forward的新标准库来传递参数,它能保持原始实参的类型。必须通过显式模板实参来调用,返回该显式实参类型的右值引用:

#include <utility>
template <typename Type> intermediary(Type &&arg)
{
    finalFcn(std::forward<Type>(arg));
}

template<typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
    f(std::forward<T2>(t2), std::forward<T1>(t1));
}

  与std::move相同,对std::forward不适用using声明是一个好主意

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值