模版与参考


在〈函数模版〉的最后,创建了printAll模版函数:

template <typename T>
void printAll(T &arr) {
   for(auto elem : arr) {
       cout << elem << " ";
   }
   cout << endl;
}

如果当时范例中的printAll(arr1)来调用,那么T会被推断为int [2],那么你可能会想,如果模版参数定义为T&&,应该是可接受 rvalue 吧!是的,然而,其实也可以接受 lvalue!

这是怎么一回事呢?这边要从简单的情境开始来探讨,首先,现在的你,应该能判断底下会显示 10:

#include <iostream> 
using namespace std; 

void foo(int &p) {
    p = 10;
}

int main() {
    int x = 5;
    foo(x);
    cout << x << endl;
    return 0; 
}

接下来,你会定义模版,模版的流程中会调用foo,底下结果会显示什么呢?

#include <iostream> 
using namespace std; 

void foo(int &p) {
    p = 10;
}

template <typename T>
void some(T t) {
    foo(t);
}

int main() {
    int x = 5;
    some(x);
    cout << x << endl;
    return 0; 
}

因为some(x)的调用,编译器创建了some(int)的版本,而不是some(int&),因此结果会显示 5,这结果对或不对,要看你透过some(x)调用时,预期会得到什么结果。

就调用一个函数而言,基本上是不该对函数的实现有任何的假设,some若给的协定是x不会被改变,那以上结果就会是对的,若some给的协定是x结果应该会改变,那模版的定义显然应该改为:

template <typename T>
void some(T &t) {
    foo(t);
}

若使用这个版本的some,方才的范例就会显示 10,可是这么一来,就无法使用some(10)这种调用了,因为 10 是个 rvalue,也许你会想要创建some模版的重载版面:

#include <iostream> 
using namespace std; 

void foo(int &p) {
    p = 10;
}

template <typename T>
void some(T &t) {
    foo(t);
}

template <typename T>
void some(const T &t) {
    foo(t);
}

int main() {
    int x = 5;
    some(x);
    some(10);
    cout << x << endl;
    return 0; 
}

是的!模版函数也可以重载,这可以解决需求,只不过,模版的实现内容一模一样,这样似乎失去了模版的意义,在〈函数模版〉中,确实有个范例,将greaterThan模版特化出greaterThan(string, string)版本,然而其意义在于,特化版本的实现内容与泛型版本不同,而在上面的范例,显然地,两个模版的实现内容是相同的。

其实只要改为以下就可以了:

#include <iostream> 
using namespace std; 

void foo(int &p) {
    p = 10;
}

template <typename T>
void some(T &&t) {
    foo(t);
}

int main() {
    int x = 5;
    some(x);
    some(10);
    cout << x << endl;
    return 0; 
}

嗯?调用some(10)时,10 是个 rvalue,因此T&&可以接受,这部份是没问题,而是some(x)x不是个 lvalue 吗?怎么行得通?

这边其实是 C++ 语言中的一个特例,如果将 lvalue 传给模版函数的T&&参数的话,T会被推断为int&,也就是说编译器首先会为some(x)创建some(int& &&)版本!

于是你马上就会想试了,那可以自已写个int& &&r之类的定义吗?创建一个 rvalue 参考的参考(reference to reference)?就像创建〈指针的指针〉那样?

不行!你不能(直接)创建参考的参考!好吧!那方才编译器为some(x)创建some(int& &&)版本又怎么说?嗯…编译器运用了它的的权能…XD

编译器运用它的权能并不是笑话,毕竟怎么看得一个程序,本来就是编译器在管的,方才编译器为some(x)创建some(int& &&)版本,就是编译器在运用它的权能,接下来编译器就运用它的另一个权能,将int& &&收合为int&,于是一切就都说得过去了…XD

开外挂来着!你要说只有编译器可以创建参考的参考,这么说也是没错,或者你可以说,C++ 语言中若要创建参考的参考,就是透过模版「间接」创建。

虽说是编译器的权能,不过总得给个收合的规则吧!对于一个模版参数t,编译器推断出类型后,会依以下情况收合:

  • X& &、X& &&、X&& & 都会收合为 X&,也就是 lvalue 参考
  • X&& && 收合为 X&&,也就是 rvalue 参考

因此方才的int& &&就收合为int&了,也就是个 lvalue 参考,编译就通过了

为了效率以及实现移动语义时的方便,C++ 11 可以创建 rvalue 参考,程序语言就是这样,为了某个需求创造了新的语法,新的语法又会创造新的需求,然后循环就开始了,语言就越发膨胀而臃肿…来看看吧!以下会编译失败:

#include <iostream> 
using namespace std; 

void foo(int &&p) {
    //...
}

template <typename T>
void some(T &&t) {
    foo(t);  // error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int'
}

int main() {
    some(10);
    return 0; 
}

编译器创建了个some(int&&)版本,因此调用some(10)没问题,可是t是个 lvalue,而现在foop是个 rvalue 参考,因此编译失败了。

t的表达式来源明明就是个 rvalue,编译器不能直接 rvalue 的性质转给foop吗?它是做得到,只不过它不知道你要不要这么做,这时它展现宽容了,如果你需要这么做,可以跟它说:

#include <iostream> 
#include <utility>
using namespace std; 

void foo(int &&p) {
    //...
}

template <typename T>
void some(T &&t) {
    foo(std::forward<T>(t));
}

int main() {
    some(10);
    return 0; 
}

utility标头文件中定义了forward,不过这名称太寻常了,建议调用时使用std::forward以避免同名问题,std::forward是在告诉编译器,将调用时表达式来源的信息转给接收的那方,就上例而言,可以看成std::forward创建了一个管道,接通了10int &&p,10 是个 rvalue,而p是个 rvalue 参考,这样就 OK 了!

std::forward是在告诉编译器,将调用时表达式来源的信息转给接收的那方,因此不仅适用于以上的情况,例如,以上是可以通过编译的:

#include <iostream> 
#include <utility>
using namespace std; 

void r(int &p) {

}

void rr(int &&p) {

}

template <typename F, typename T>
void some(F f, T &&t) {
    f(std::forward<T>(t));
}

int main() {
    int x = 10;
    some(r, x);
    some(rr, 10);
    return 0; 
}

在这边运用了传递函数,这之后就会说明,简单来说,函数是可以传递的,在some(r, x)时,编译器会创建some(void (*f)(int&), int& && t)的版本,也就是T被推断为int&,而后int& &&t会被收合为int&,接着的f(std::forward<T>(t))内容编译器会创建为f(std::forward<int&>(t)),也就是说,可以看成xint &p之间创建了一个管道,因此可以通过编译。

至于some(rr, 10)时,编译器会创建some(void (*f)(int&&), int &&t)的版本,接着的f(std::forward<T>(t))内容编译器会创建为f(std::forward<int>(t)),也就是说,可以看成10int &&p之间创建了一个管道,因此可以通过编译。


展开阅读全文