玩命加载中 . . .

16.2-模板实参推断


16.2.1 类型转换与模板类型参数

函数模板只有两种情况的类型转换

  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)
fref(s1, s2);   // 调用fref(const string&, const string&)

fobj是按值传递,需要对实参进行拷贝,所以实参是不是const都可以
fref是按引用传递,将非consts1转换为const是允许的

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

两个数组大小不同,所以是不同类型
调用fobj时,数组被转换为指针,所以大小不同没关系
调用fref时,因为形参是引用,所以数组不会被转换为指针,大小又不一样,所以调用失败

16.2.2 函数模板显示实参

指定显式模板实参

定义一个允许用户控制返回类型的函数模板

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

这样每次调用sum都必须为T1提供一个显式模板实参

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

还要注意模板参数的顺序,如果返回值是T3,就必须为T1T2T3都指定实参

对于模板类型参数已经显式指定了的函数实参,也进行正常的类型转换

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

long lng;
compare(lng, 1024); // 错误,模板参数不匹配 compare(long, int)
compare<long>(lng, 1024);   // 正确,compare(long, long)
compare<int>(lng, 1024);    // 正确,compare(int, int)

16.2.3 尾置返回类型与类型转换

在模板参数中让用户指定返回类型会给用户增加编程负担,可以使用尾置类型来获取返回类型

比如,我们需要传入容器的迭代器,返回迭代器指向的元素的引用

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

fcn的返回类型与解引用beg参数的结果类型相同,解引用运算符返回一个左值,因此通过decltype推断的类型为beg表示的元素的类型的引用

如果要返回迭代器指向元素的拷贝,可以用标准库的remove_reference

template<typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type {
    return *beg;
}

注意:type是一个类的类型成员,所以必须使用typename来告知编译器,type表示一个类型

16.2.4 函数指针和实参推断

可以用函数模板给函数指针赋值

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

这里函数指针pf1的参数类型是int,所以用函数模板给它赋值时,编译器会使用pf1的参数类型来推断函数模板实参的类型,所以pf1指向compareint版本实例

如果不能从函数指针类型推断出模板实参,则报错

// func重载了两个版本
void func(int(*)(const int&, const int&));
void func(int(*)(const string&, const string&));
func(compare);  // 错误,不知道调用哪个版本

这里func无法根据形参来推断compare的类型参数,所以只能显式地指定类型参数

func(compare<int>);

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

16.2.5 模板实参推断和引用

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

  • 模板参数是普通左值引用时,只能传递左值,可以是const类型,也可以不是,如果是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&时,可以传递任何实参
template<typename T> void f2(const T&);
f2(i);  // i是一个int,所以T推断为int
f2(ci); // ci是一个const int,所以T推断为int,const已经是类型的一部分了
f2(5);  // 一个const &参数可以绑定到右值,所以T推断为int

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

  • 当模板参数是右值引用,可以传递右值,也可以是左值
template<typename T> void f3(T&&);
f3(42);     // 实参是一个int类型的右值,T推断为int

引用折叠和右值引用参数

正常不能把一个右值引用绑定到一个左值,但有两个例外

  1. 当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数时(如T&&),编译器推断模板类型参数为实参的左值引用类型。因此,当调用f3(i)时,编译器推断T的类型为int&
  2. 如果我们间接创建一个引用的引用,则这些引用形成了折叠
    • X& &X& &&X&& &都折叠成类型X&
    • 类型X&& &&折叠成X&&

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

这样,给f3传递左值时,编译器推断T为左值引用,并使用引用折叠

f3(i);  // 实参是一个左值,T推断为int&
f3(ci); // 实参是一个左值,T推断为const int&

f3的实例化类似于

void f3<int&>(int& &&); // T是int&
折叠成
void f3<int&>(int&);

这两个规则导致了两个重要结果

  • 如果一个函数参数是一个指向模板类型参数的右值引用(T&&),则它可以被绑定到一个左值
  • 如果实参是一个左值,则推断出的模板类型将是一个左值引用,且函数参数将被实例化为一个普通左值引用参数(T&

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

16.2.6 理解std::move

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

string s1("hi"), s2;
s2 = std::move(string("bye"));  // 实参是右值
s2 = std::move(s1);     // 实参是左值

1、传递右值

  • 推断T的类型为string
  • remove_referencestring进行实例化
  • remove_reference<string>type成员是string
  • move的返回类型是string&&
  • move的函数参数t的类型是string&&

实例化为

string&& move(string&& t) {     // 右值
    return static_cast<string&&>(t);
}

2、传递左值

  • T的类型为string&
  • remove_referencestring&进行实例化
  • remove_reference<string&>type成员是string
  • move的返回类型仍是string&&
  • move的函数参数t的类型为string& &&,会折叠为string&

实例化为

string&& move(string& t) {  // 左值
    return static_cast<string&&>(t);    // 转换为右值
}

这里用static_cast显式地将一个左值转换为右值引用

16.2.7 转发

template<typename F, typename T1, typename T2>
void flip1(F func, T1 t1, T2 t2) {
    func(t2, t1);
}

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

如果直接调用f函数

int i = 10;
f(42, i);   // 调用结束后i=11,因为是按引用传递

但是在flip1里面调用就不一样了

int j = 10;
flip1(f, j, 42);    // 调用结束后j=10,没有改变

因为j传递给t1是按值传递,所以对t1的改动不会影响到j

如果想对j改动,就要按引用传递,所以flip1函数必须能同时支持左值实参和右值实参,要实现这样的功能就需要用右值模板参数

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

int j = 10;
flip2(f, j, 42);    // 调用结束后j=11

这样,传递左值j时,T推断为int&,经过引用折叠,t1的类型为int&,这样对t1的修改就是对j的修改

传递右值42时,T2推断为intt2的类型为int&&

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

假设现在不调用f,改成调用g,把第一个参数改成右值引用

template<typename F, typename T1, typename T2>
void flip2(F func, T1&& t1, T2&& t2) {
    func(t2, t1);
}
void g(int&& v1, int& v2) { // 第一个参数是右值引用
    cout << v1 << " " << v2 << endl;
}

flip2(g, j, 42);    // 错误,不能将左值实参的t2绑定到右值形参v1

这里传递给t2是一个右值42,所以t2的类型为int&&,但是t2是变量,是左值,传递给g时,因为v1是右值引用,不能绑定到一个左值,所以报错

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

// 转发左值
template<typename T>
T&& forward(typename std::remove_reference<T>::type& t) 
{
    return static_cast<T&&>(t); 
}

// 转发右值
template<typename T>
T&& forward(typename std::remove_reference<T>::type&& t) 
{
    return static_cast<T&&>(t);
}

我们希望保持42这个右值的类型信息,不要让它变成左值,可以使用std::forwardforward必须通过显式模板参数来调用,返回该实参类型的右值引用,即,forward<T>的返回类型是T&&

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

flip2(g, j, 42);    // 正确

这样,T2推断为intforward返回类型是int&&,可以传递给函数g中的右值引用参数v1;而T1推断为int&forward的返回类型为int& &&,折叠后是int&,可以传递给函数g中的左值引用参数v2


文章作者: kunpeng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 kunpeng !
  目录