左值右值

基本概念:

左值:在內(nèi)存中有可以訪問的地址,對象是一個左值。

右值:不可以取地址,整數(shù)10是個右值。

引用:對象的別名,沒有創(chuàng)建新的對象,僅僅給已經(jīng)存在的對象賦予了一個新的名字。

  1. 引用是對象的別名,對于引用的一切操作都是對對象的操作;

  2. 引用自身從概念上沒有大?。ɑ蛘呔褪菍ο蟮拇笮。坏迷趥鬟f或需要存儲時,其傳遞或存儲的大小為地址的大小。

  3. 引用必須初始化;

  4. 引用不可能重新綁定;

  5. 將指針所指向的對象綁定到一個引用時,需要確保指針非空。

  6. 任何引用類型的變量,都是左值。

四種類型引用:

類型 例子 備注
const lvalue refrence Foo foo(10); const Foo& ref = foo;
const rvalue refrence const Foo& ref = Foo(10);
non-const lvalue refrence Foo foo(10); Foo& ref = foo;
non-const rvalue refrence Foo&& ref=Foo(10); C++11才開始有

move語義:

C++11 之前,只有 copy 語意,這對于極度關(guān)注性能的語言而言是一個重大的缺失。

對move 語意的急迫需求,到了 C++11 終于被引入。其直接的驅(qū)動力很簡單:在構(gòu)造或者賦值時,如果等號右側(cè)是一個中間臨時對象,應(yīng)直接將其占用的資源直接 move 過來(對方就沒有了)。

但問題是,如何讓一個構(gòu)造函數(shù),或者賦值操作重載函數(shù)能夠識別出來這是一個臨時變量?

/////////////hello.cpp/////////////////
#include <iostream>
using namespace std;
struct Foo
{
    Foo(){ cout << "Foo()" << endl; }
    Foo(const Foo&ref){ cout << "Foo(const Foo&)" << endl; } // copy ctor
    Foo(Foo&& ref){ cout << "Foo(Foo&&)" << endl; }          // move ctor
    Foo& operator=(const Foo& rhs){cout << "Foo& operator=(const Foo& rhs)" << endl; } // copy assignment
    Foo& operator=(Foo&& rhs){cout << "Foo& operator=(Foo&& rhs)" << endl; }           // move assignment 
    ~Foo(){ cout << "~Foo()" << endl; }
};

int main(int argc, char* argv[])
{
    cout<<"=========="<<endl;
    Foo foo1 = Foo();
    cout<<"=========="<<endl;
    foo1 = Foo();
    cout<<"=========="<<endl;
    Foo  foo2 = foo1;
    cout<<"=========="<<endl;
    foo2 = foo1;
    getchar();
    return 1;
}

實參類型為non-const lvalue reference、const lvalue referenceconst rvalue reference可以匹配到copy ctorcopy assignment。

實參類型為non-const rvalue reference 才能匹配到 move ctormove assignment 。

通過這樣的方式,讓 Foo foo1 = Foo()foo1 = Foo()這樣的表達式,都可以匹配到 move 語意的版本。

與此同時,讓 Foo foo2 = foo1foo2 = foo1 這樣的表達式,依然使用 copy 語意的版本。

達到以上效果需要編譯時加上-fno-elide-constructors,以此關(guān)閉編譯器省略創(chuàng)建一個只是為了初始化另一個同類型對象的臨時對象的優(yōu)化。

root@ubuntu-Vostro-3268:/mnt/zpp# g++ hello.cpp  -fno-elide-constructors
root@ubuntu-Vostro-3268:/mnt/zpp# 
root@ubuntu-Vostro-3268:/mnt/zpp# 
root@ubuntu-Vostro-3268:/mnt/zpp# ./a.out 
==========
Foo()
Foo(Foo&&)
~Foo()
==========
Foo()
Foo& operator=(Foo&& rhs)
~Foo()
==========
Foo(const Foo&)
==========
Foo& operator=(const Foo& rhs)

使用編譯器優(yōu)化時:

root@ubuntu-Vostro-3268:/mnt/zpp# g++ hello.cpp  
root@ubuntu-Vostro-3268:/mnt/zpp# ./a.out 
==========
Foo()
==========
Foo()
Foo& operator=(Foo&& rhs)
~Foo()
==========
Foo(const Foo&)
==========
Foo& operator=(const Foo& rhs)

練習:

struct Foo
{
    Foo(int a) :a(a){}
    int a;
};

void test1(Foo&& f)
{
// 對于任何類型為 右值引用的變量(當然也包括函數(shù)參數(shù)),只能由右值來初始化;
}

void test2(Foo& f)
{
//  一個右值,不能被 T& 類型的參數(shù)匹配;畢竟這種可以修改的承諾。而修改一個調(diào)用后即消失的臨時
//  對象上,沒有任何意義,反而會導(dǎo)致程序員犯下潛在的錯誤,因而還是禁止了最好
}

void test3(const Foo& f)
{

}   

Foo f1(1);
test1(f1);  // wrong  cannot bind ‘Foo’ lvalue to ‘Foo&&’ 不能將一個左值綁定到右值引用
test2(f1);  // ok
test3(f1);  // ok

test1(Foo{1});  // ok      Foo{1}是右值
test2(Foo{1});  // wrong   這種做法無意義,invalid initialization of non-const reference of type ‘Foo&’ from an rvalue of type ‘Foo’
test3(Foo{ 1 });  // ok

// ref是左值雖然其類型是右值引用; 
// 一個類型為 右值引用的變量,一旦被初始化之后,臨時對象的生命將被擴展,會在其被創(chuàng)建的 scope 內(nèi)始終有效。
// 看似 ref 被定義的類型為 右值引用,但這僅僅約束它的初始化:只能從一個 右值進行初始化。
// 但一旦初始化完成,它就和一個 左值引用再也沒有任何差別:都是一個已存在對象的 標識。
Foo&& ref = Foo{1}; 
test1(ref);    // wrong  ref是左值,test1的形參為右值引用,右值引用的變量只能由右值來初始化 cannot bind ‘Foo’ lvalue to ‘Foo&&’
test2(ref); // ok  
test3(ref); // ok

速亡值:

只有右值臨時對象可以初始化右值引用變量,從而也只有右值臨時變量能夠匹配參數(shù)類型為 右值引用(T&&)的函數(shù),包括 move 構(gòu)造函數(shù)。

如果程序員就是想把一個左值 move 給另外一個對象,該怎么辦?最簡單的選擇是通過 static_cast 進行類型轉(zhuǎn)換:

Foo foo{10};           // foo為左值
Foo&& ref = Foo{10};   // ref為左值  類型為右值引用
Foo obj1 = static_cast<Foo&&>(foo); // move 構(gòu) 造
Foo obj2 = static_cast<Foo&&>(ref); // move 構(gòu) 造

我們之前說過,只有 右值,才可以用來初始化一個 右值引用類型的變量,因而也只有 右值才能匹配 move構(gòu)造。

所以,static_cast<Foo&&>(foo) 表達式,肯定是一個 右值。

但同時,它返回的類型又非常明確的是一個 引用,而這一點又不符合 右值的定義。

因為,所有的右值,都必須是一個 具體類型,不能是不完備類型,也不能是抽象類型,但 引用,無論左值引用,還是右值引用,都可以是不完備類型的引用或抽象類型的引用。這是 左值才有的特征。

對于這種既有左值特征,又和右值臨時對象一樣,可以用來初始化右值引用類型的變量的表達式,只能將其歸為新的類別。C++11 給這個新類別命名為 速亡值 (eXpiring value,簡稱 xvalue)。

而將原來的 右值,重新命名為 純右值。而 速亡值純右值合在一起,稱為 右值,其代表的含義是,所有可以直接用來初始化 右值引用類型變量的表達式。

同時,由于 速亡值又具備左值特征:可以是不完備類型,可以是抽象類型,可以進行運行時多態(tài)。所以,速亡值又和 左值一起被歸類為 泛左值(generalized lvalue, 簡稱 glvalue)。

? 類型為 右值引用的變量,只能由 右值表達式初始化;

? 右值包括 純右值速亡值,其中 速亡值的類型是 右值引用;

? 類型為 右值引用的變量,是一個 左值,因而不能賦值給其它類型為 右值引用的變量,當然也不能匹配參數(shù)類型為 右值引用的函數(shù)。

參考文獻:

  1. Understanding Modern C++
  2. -fno-elide-constructors參數(shù)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容