1. 程式人生 > >C++11中的universal引用和右值引用

C++11中的universal引用和右值引用

stackoverflow上有個問題:Why “universal references” have the same syntax as rvalue references? 就是說為什麼這倆的形式都是T&&(T表示一個型別)。其中有一個回答很好,回答如下:

I think it happened the other way around. The initial idea was to introduce rvalue-references into the language, meaning that “the code providing the double-ampersand reference does not care about what will happen to the referred-to object”. This permits move semantics. This is nice.
Now. The standard forbids constructing a reference to a reference, but this was always possible. Consider:

    template<typename T>
    void my_func(T,T&){...}
    ...
    my_func<int&>(a,b);

In this case the type of the second parameter should be int & &, but this is explicitly forbidden in the standard. So the references have to be collapsed, even in C++98. In C++98, there was only one kind of reference, so the collapsing rule was simple:
& & -> &


Now, we have two kinds of references, where && means “I don’t care about what may happen to the object”, and & meaning “I may care about what may happen to the object, so you better watch what you’re doing”. With this in mind, the collapsing rules flow naturally: C++ should collapse referecnces to && only if no one cares about what happens to the object:
& & -> &

& && -> &
&& & -> &
&& && -> &&
With these rules in place, I think it’s Scott Meyers who noticed that this subset of rules:
& && -> &
&& && -> &&
Shows that && is right-neutral with regards to reference collapsing, and, when type deduction occurs, the T&& construct can be used to match any type of reference, and coined the term “Universal reference” for these references. It is not something that has been invented by the Committee. It is only a side-effect of other rules, not a Committee design.
And the term has therefore been introduced to distinguish between REAL rvalue-references, when no type deduction occurs, which are guaranteed to be &&, and those type-deduced UNIVERSAL references, which are not guaranteed to remain && at template specialization time.

也就說,其實並沒有什麼universal引用,它只是C++引用摺疊規則的一個副產物罷了。而且這個一般universal引用只能在模板中發揮作用,一旦一個非模板函式的引數型別宣告成T&&的形式,那個T已經是固定的了,所以T&&就是固定的,也就只能表示右值引用。而在模板中,那個T是不確定的,如果T是一個左值引用型別,即T = S&,那麼T&&就等價於S& &&,根據上面的引用摺疊規則,& && -> &,那麼S& &&型別就是S&,也就是左值引用,其他同理。

下面看一個例子:

void f(int&& a) {
    std::cout << "rvalue" << std::endl;
}

void f(int& a) {
    std::cout << "lvalue" << std::endl;
}

template<typename T>
void test(T&& a) {
    f(forward<T>(a));
}

template<typename T>
T&& forward(std::remove_reference_t<T>& arg)
{
    return static_cast<T&&>(arg);
}

int main()
{

    int a = 10;
    test(a);
    test(std::move(a));

    //system("pause"); //for windows VS to pause to see the output
    return 0;
}

這裡實現了一個非常非常簡單的完美轉發(perfect forwarding)。我們下面一步一步分析一下程式碼的執行過程:

  • 首先宣告一個int變數,這是一個左值
  • 然後呼叫test(a)。a的型別是int,你可能認為test函式模板應該例項化成如下,畢竟傳遞進去的是一個int型別:
//typename T 被推斷成 int
void test(int&& a){
    f(forward<int>(a));     
}
  • 但是如果是這樣,就不對了啊,明明傳遞進去一個左值,而test模板對其例項化的結果居然接收一個右值引用型別,這肯定不對啊!所以,當引數是一個左值時,在模板型別推斷的時候,左值會被推斷成左值引用,不管傳遞的引數是左值型別還是左值引用,通通變成左值引用。於是正確的模板例項應該是這樣:
//typename T 被推斷成 int&
void test(int&  a){   //int& && -> int&
    f(forward<int&>(a));        
}
  • 這樣的話,就進入了forward函式,其模板引數被指定成int&,forward被例項化成這樣:
//typename T 是int&,
int& forward(int& arg)  //std::remove_reference_t<int&> 就是 int
{
    return static_cast<int&>(arg);   //int& && -> int&,返回型別也是一樣的原理
}
  • 於是將a型別轉換成一個左值引用,傳遞給f,所以呼叫的是接收左值引用型別的f過載,列印lvalue
  • 然後再呼叫main函式裡面的test(std::move(a))。std::move的作用就是把一個變數轉換成一個右值,裡面做的工作其實就是一個型別轉換。test和forward模板例項化成這樣:
//typename T 被推斷成 int
void test(int&&  a){  
    f(forward<int>(a));     
}
//typename T 是int,
int&& forward(int& arg)  //std::remove_reference_t<int> 就是 int
{
    return static_cast<int&&>(arg);  
}
  • 所以返回一個右值引用,從而呼叫接收右值引用的f過載,打印出rvalue。

這裡簡要說明一下為什麼需要完美轉發。因為在test函式裡面,其引數a是一個左值,不管它前面的型別是右值型別還是左值型別,它都是一個左值。而如果用這個a呼叫f的話,只能呼叫f的左值過載版本,永遠呼叫不到其右值過載版本。至於為什麼a是一個左值,簡單說就是隻要有名字的變數都是左值,具體可以參考右值引用。而完美轉發則可以實現傳遞進來什麼型別,我就返回什麼型別,所以就可以利用其實現對不同過載版本的f的呼叫。

下面我簡單修改一下mian函式:

...     //as before
int main()
{
    int a = 10;
    int &b = a;
    int&& c = std::move(a);
    test(b);//這裡b是一個左值引用
    test(c);//c是一個右值引用

    //system("pause"); //for windows VS to pause to see the output
    return 0;
}

這裡,有兩點想說明:

  1. b是一個左值引用,但是正如前面所說,只要是左值,test模板的推斷型別都是int&,所以test(b)呼叫的是f的左值版本,列印lvalue
  2. c雖然是一個右值引用,但是它的的確確是一個有名字的變數,那麼它實際上就是一個左值,所以test(c)呼叫的也是f的左值版本,列印lvalue