能夠經過模板或者 typedef 中的類型操做構成引用的引用,可是C++不認識多個&
的,因此就產生一個規則,左值引用 &
, 右值引用 &&
,在結合的時候,能夠把左值引用看做是顯性基因,只要有左值引用,那麼結合就摺疊成左值引用,要兩個都是隱形基因(&&
)的狀況,纔不會進行摺疊。c++
typedef int& lref; typedef int&& rref; int n; lref& r1 = n; // r1 的類型是 int& lref&& r2 = n; // r2 的類型是 int& rref& r3 = n; // r3 的類型是 int& rref&& r4 = 1; // r4 的類型是 int&&
寫個小例子就能夠看出效果了,普通函數的狀況以下,模板的示例見 std::forward 分析app
int foo(int &&arg) { std::cout << "int &&\n"; } // 不會被調用 int foo(int &arg) {std::cout << "int &\n";} // 兩個函數只能存在一個 // int foo(int arg) { std::cout << "int\n"; } int main() { int &&rref = 1; foo(rref); // int 或者 int & }
指針和引用常常會一塊兒出現,我的的理解函數
經過一個例子就能夠看的很清楚,二者都是 訪問地址 來實現的,但因爲歷史緣由咱們一說到地址就會想到指針。this
void ref() { int value = 13; int &lref = value; lref = 9; int *p = nullptr; p = &value; *p = 21; } _Z3refv: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $13, -20(%rbp) leaq -20(%rbp), %rax # 取 value 的地址 &value movq %rax, -8(%rbp) # 將 value 的地址轉移,這兩步能夠不須要的 movq -8(%rbp), %rax movl $9, (%rax) # 賦值 lref = 9 movq $0, -16(%rbp) # 指針初始化 leaq -20(%rbp), %rax # 同上,取地址 movq %rax, -16(%rbp) movq -16(%rbp), %rax movl $21, (%rax) # 賦值 *p = 21 nop popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc
在使用上來講,引用優於指針的地方在於,引用避免了空指針的判斷,而且在使用上和值語義相近。
google 的 coding style 上也有針對引用和指針參數的規範,入參若是不可以被改變的話,使用 const T &
,若是是須要使用指針或者參數可變的狀況下使用指針入參。google
// 形式以下 void do_something(const std::string& in, char *out);
左值引用的定義清晰,就是既存對象的別名,看成披着地址的皮來使用就能夠,而且也能延長生命週期(const T &
接收),見延長右值引用分析。
懸置引用在使用不當的時候可能出現,以下spa
struct Foo { Foo() : value(13) {} ~Foo() { value = -1; } int value; }; Foo &get_foo() { Foo f; return f; } int main() { Foo &f = get_foo(); } // 反彙編,只截取 get_foo() .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 pushq %rbx subq $24, %rsp .cfi_offset 3, -24 leaq -20(%rbp), %rax // 對象 f 的地址 movq %rax, %rdi // 構造函數的隱藏參數 call _ZN3FooC1Ev // 調用構造函數 movl $0, %ebx leaq -20(%rbp), %rax movq %rax, %rdi call _ZN3FooD1Ev // 析構函數 movq %rbx, %rax // 最後返回的是 rax(rax = rbx),可是這個 rbx 是沒有來源的,訪問直接段錯誤 addq $24, %rsp popq %rbx popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc
當出現這種懸置引用的時候,再去訪問就不知道是什麼錯誤了,好消息是編譯器能夠識別這個問題而且發出警告的。指針
右值引用就是爲了延長生命週期而生的,這裏再扯一下,左值引用也是能夠作到這一點的,可是不可以經過左值引用修改。
拿一下 cppreference 中的例子,右值引用是經過 &&
使得編譯器指令重排而延長生命週期的,而左值引用是 const T &
進行py交易的,code
在以上函數增長一個友元函數,重載 +
操做符。對象
friend Foo operator+(const Foo &lhs, const Foo &rhs) { Foo foo; foo.value = lhs.value + rhs.value; return foo; } int main() { Foo f1; const Foo &lref = f1 + f1; // rf.value = 1; Foo &&rref = f1 + f1; // 臨時變量 f1 + f2 的引用 rref.value = 4; // 相同 } // 反彙編取重載函數和main函數代碼 _ZplRK3FooS1_: .LFB6: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $32, %rsp movq %rdi, -8(%rbp) // rdi 是構造函數的第一個參數,當函數返回對象時,就是這樣作的 movq %rsi, -16(%rbp) // lhs movq %rdx, -24(%rbp) // rhs movq -8(%rbp), %rax movq %rax, %rdi call _ZN3FooC1Ev // 調用構造函數 movq -16(%rbp), %rax movl (%rax), %edx // lhs.value movq -24(%rbp), %rax movl (%rax), %eax // rhs.value addl %eax, %edx // edx = lhs.value + rhs.value movq -8(%rbp), %rax movl %edx, (%rax) // foo.value = edx nop movq -8(%rbp), %rax // return foo leave .cfi_def_cfa 7, 8 ret .cfi_endproc main: .LFB8: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $32, %rsp leaq -28(%rbp), %rax // 取 f1 的地址 movq %rax, %rdi call _ZN3FooC1Ev // Foo f1; leaq -24(%rbp), %rax // 重載函數內的 臨時對象,當重載函數返回對象時,編譯器便把對象指針傳進去 leaq -28(%rbp), %rdx // rhs,f1 leaq -28(%rbp), %rcx movq %rcx, %rsi // lhs,f1 movq %rax, %rdi call _ZplRK3FooS1_ // 調用重載函數 leaq -24(%rbp), %rax movq %rax, -8(%rbp) leaq -20(%rbp), %rax // 第二次調用的重載函數內的 臨時對象指針 leaq -28(%rbp), %rdx // rhs,f1 leaq -28(%rbp), %rcx movq %rcx, %rsi // lhs,f1 movq %rax, %rdi call _ZplRK3FooS1_ // 第二次調用重載函數 leaq -20(%rbp), %rax // 這兩個值是相等的,也就是返回的臨時對象指針 movq %rax, -16(%rbp) movq -16(%rbp), %rax movl $4, (%rax) // rref.value = 4; leaq -20(%rbp), %rax movq %rax, %rdi call _ZN3FooD1Ev // 析構函數被移動到做用域以外也就是main函數裏面了 leaq -24(%rbp), %rax movq %rax, %rdi call _ZN3FooD1Ev leaq -28(%rbp), %rax movq %rax, %rdi call _ZN3FooD1Ev movl $0, %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc
能夠看到 &&
和 const T &
產生的彙編代碼幾乎是同樣的,二者都提供了常量引用的語義,是編譯器的實現也在函數返回對象的狀況下模糊了這二者的區別(生成彙編代碼),因此在有些狀況下,在未提供 f(T &&)
重載則會調用 f(const T &)
。可是區別在於常量左值引用是不可修改的。生命週期
一些函數提供了兩個引用的重載版本,如 std::vector::push_back()
,容許自動選擇copy構造函數和移動構造函數。
左值
簡單粗暴的理解就是在操做符的左邊的表達式,可是C++的概念比較的多,例如,++i 這個是左值,i++ 就是純右值了,字符串常量也沒有想到是左值吧,由於不能修改,因此不能存在於表達式的左邊。
cppreference 中的概念陳述的很是多,簡單而言就是有分配內存的對象就是左值,只有這種狀況纔可以用於初始話左值引用(字符串常量,const char *
)。
純右值
取不到地址的表達式,如內建類型值,this指針,lambda
亡值
差很少能夠理解爲,做爲一個臨時量,內存中存在數據,若是不延長生命週期的話,該對象就會被銷燬。std::move 產生的就是亡值。
而後上面的種類繁多,又有混合類別產生:
右值引用變量的名稱是左值,而若要綁定到接受 右值引用參數的重載,就必須轉換到亡值,這是移動構造函數與移動賦值運算符典型地使用 std::move 的緣由。
函數名稱和目的相關,但內部實現沒有什麼移動的操做,就一個轉換類型,見 libstdcxx 源碼。
template<typename _Tp> constexpr typename std::remove_reference<_Tp>::type&& move(_Tp&& __t) noexcept { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
轉發引用利用 std::forward
保持實參值類型進行完美轉發,完美轉發詳細的說一下,它的實現也不是很複雜,有兩個重載函數,實際上都是類型轉換,
// 轉發左值爲左值或右值,依賴於 T template <typename _Tp> constexpr _Tp && forward(typename std::remove_reference<_Tp>::type &__t) noexcept { return static_cast<_Tp &&>(__t); } // 轉發右值爲右值並禁止右值的轉發爲左值 template <typename _Tp> constexpr _Tp && forward(typename std::remove_reference<_Tp>::type &&__t) noexcept { static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument substituting _Tp is an lvalue reference type"); return static_cast<_Tp &&>(__t); }
參考上面的 引用摺疊 ,如下給定例子的參數類型推導:
template <typename T> void foo(const T &arg) { std::cout << "const T &\n"; } template <typename T> void foo(T &arg) { std::cout << "T &\n"; } template <typename T> void foo(T &&arg) { std::cout << "T &&\n"; } template <typename T> void wrapper(T &&arg) { foo(std::forward<T>(arg)); } int main() { Foo f1; const Foo f2; wrapper(f1); // T & wrapper(f1 + f1); // T && wrapper(f2); // const T & }
Foo
, 這樣 std::forward
就把右值引用轉發給 fooconst
限定左值,則推導 T 爲 const Foo &
,在引用摺疊下 std::forward
將 const 左值引用傳遞給 fooFoo &
,在引用摺疊下 std::forward
將非 const 左值引用傳遞給 foo另外,對類型的推導過程都是在編譯期完成的,不一樣的限定或者引用類型的c++代碼生成的彙編代碼沒有區別,爲了編譯期匹配到正確的函數調用。