在〈函式模版〉的最後,建立了 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,而現在 foo
的 p
是個 rvalue 參考,因此編譯失敗了。
t
的運算式來源明明就是個 rvalue,編譯器不能直接 rvalue 的性質轉給 foo
的 p
嗎?它是做得到,只不過它不知道你要不要這麼做,這時它展現寬容了,如果你需要這麼做,可以跟它說:
#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
建立了一個管道,接通了 10
與 int &&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))
,也就是說,可以看成 x
與 int &p
之間建立了一個管道,因此可以通過編譯。
至於 some(rr, 10)
時,編譯器會建立 some(void (*f)(int&&), int &&t)
的版本,接著的 f(std::forward<T>(t))
內容編譯器會建立為 f(std::forward<int>(t))
,也就是說,可以看成 10
與 int &&p
之間建立了一個管道,因此可以通過編譯。