模版與參考


在〈函式模版〉的最後,建立了 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 之間建立了一個管道,因此可以通過編譯。