主頁 > 後端開發 > MordernC++之左值(參考)與右值(參考)

MordernC++之左值(參考)與右值(參考)

2023-04-10 07:24:00 後端開發

左值與右值

C++中左值與右值的概念是從C中繼承而來,一種簡單的定義是左值能夠出現再運算式的左邊或者右邊,而右值只能出現在運算式的右邊

int a = 5; 	// a是左值,5是右值 
int b = a;	// b是左值,a也是左值
int c = a + b;	// c是左值,a + b是右值

另一種區分左值和右值的方法是:有名字、能取地址的值是左值,沒有名字、不能取地址的值是右值,比如上述陳述句中a,b, c是變數可以取地址,所以是左值,而5和a + b無法進行取地址操作,因此是右值,C++中左值與右值的一個主要的區別是:左值可以被修改,而右值不可修改

左值參考與右值參考

了解了左值與右值的概念后,接下來介紹下C++中的左值參考與右值參考,左值參考很簡單,就是一個變數的別名,系結到一個左值上:

int a = 1;
int& b = a; 	//a = 1,b = 1
b = 2;		// a = 2,b = 2

這里b就等于a,在匯編層面其實和普通的指標一樣,對參考的修改(b)也會修改到被參考的物件(a),需要注意的是,因為參考實際是一個別名,因此必須初始化,即告訴編譯器是那個具體物件的別名,因此下列左值參考都是錯誤的:

int& a;		// 錯誤!左值參考必須初始化
int& b = 10;	// 錯誤!左值參考不能以臨時變數初始化(臨時變數沒有地址)

右值參考是C++11中新增的特性,顧名思義,右值參考就是用來系結到右值的參考,一個右值被系結到右值參考之后,原本需要被銷毀的此右值生命周期會延長至系結它的右值參考的生命周期,在匯編層面,右值參考和const參考所做的事情是一樣的,即產生臨時量來存盤常量,但是右值參考可以進行讀寫操作,而const參考只能進行讀操作,系結右值參考使用&&,具體使用如下:

int a = 5;
int& b = a;		// 正確!b是一個左值參考
int&& c = 6;		// 正確!c是一個右值參考,系結到右值6
int&& d = a * 2;	// 正確!d是一個右值參考,系結到右值a * 2
int&& e = i;		// 錯誤!不能將左值系結到右值參考
int& f = 7;		// 錯誤!不能將右值系結到左值參考
const int& g = a * 3;	// 正確!可以將右值系結到const 左值參考

可以看到我們雖然不能將右值系結到左值參考,但是可以將右值系結到const左值參考
注意: 變數運算式都是左值!,變數可以看作是只有一個運算物件而沒有運算子的運算式,跟其他運算式一樣,變數運算式也有左值/右值屬性,變數運算式都是左值,因此我們不能將一個右值參考系結到一個右值參考型別的變數上,

int&& a = 5;	// 正確!a是一個右值參考
int&& b = a;	// 錯誤!a是一個左值,不能系結到右值參考

這里雖然a是右值參考型別,但是確實一個左值,因此無法系結到右值參考b上,因為在C++中,右值一般是臨時物件,但是系結到右值參考之后,其生命周期變長了,因此a是一個左值,我們不能將一個右值參考直接系結到一個變數上,即使是這個變數是右值參考型別也不行,具體的這個問題在后續的介紹forward的時候會詳細說明,

左值/右值參考的模板實參推斷

在另一篇文章中介紹了C++的模板型別推斷的幾種型別,可以總結為以下三種:

  1. ParamType 是一個指標或者參考(&),但是不是通用參考(&&)
  2. ParamType是一個通用參考(&&)
  3. ParamType既不是指標,也不是參考(&)或者通用參考(&&)

從左值參考函式引數推斷型別

當一個函式引數是模板的左值參考(T&)時,根據系結規則,只能傳遞一個左值實參,這個左值實參可以時const型別,也可以不是,如果實參時const的,那么T就會被推導為const型別

template<typename T>
void func(T& param);

int a = 0;
const int b = a;
func(a);	// T被推導為int,param型別為int&
func(b);	// T被推導為const int,param型別為const int&
func(5);	// 錯誤!實參必須是一個左值!

如果一個函式的型別時const T&,那么根據系結規則,可以傳遞任何型別的實參:const或者非const,左值或者右值,由于函式型別本身已經是const,因此T的推導結果不會是一個const,因為const已經是函式引數型別的一部分了,

template<typename T>
void func(const T& param);

int a = 0;
const int b = a;
func(a);	// T被推導為int,param型別為const int&
func(b);	// T被推導為int,param型別為const int&
func(5);	// 正確!const T&可以系結一個右值,T為int

可以看到,當函式引數型別為const T&時,可以接受一個右值實參,而函式引數型別為 T& 時是不可以的,

從右值參考函式引數推斷型別

當一個函式的引數是一個右值參考(T&&)時,根據系結規則可以傳遞一個右值實參,類似左值參考推導,右值參考推導得到的T的型別為右值的型別:

template<typename T>
void func(T&& param);

func(5);	// 實參5為右值,T被推導為int型別

與不能給右值參考賦值左值不同,右值參考函式的模板實參卻可以接受一個左值的輸入,當我們將一個左值傳遞給函式的右值參考引數時,且此右值參考指向模板引數型別(T&&)時,編譯器推導模板型別引數為實參的左值參考型別:

template<typename T>
void func(T&& param);

int a = 1;
func(a);	// T被推導為int&,而不是int

如上述推導所示,當傳入一個左值a時,T被推導為int&,而不是int,對應的param的型別為int& &&,根據參考折疊的規則,int& &&被折疊為int&,

參考折疊規則
T& & ,T& && 和T&& &都會被折疊為T&
T&& &&被折疊為T&&

參考折疊的規則告訴我們:如果一個函式的引數時指向模板引數型別的右值參考(如T&&),則可以傳遞給它任意型別的實參,如果傳遞的左值實參,那么T將會推導成為一個左值參考,函式引數被實體化為一個普通的左值參考(T&),這種參考叫做“通用參考”

右值參考與通用參考

C++中T&&有兩種不同的意思,第一種是右值參考,用于系結到右值上,它們主要存在的原因是為了宣告某個物件可以被移動,T&&的第二層意思是,它既可以是一個右值參考,也可以是一個左值參考,這種參考在代碼里看起來像是右值參考(T&&),又可以表現的像是左值參考(T&),它既可以系結到右值,也可以系結到左值,還可以系結到const和no_const物件上,幾乎可以系結到任何東西,這種參考叫做“通用參考”,在兩種情況下會出現通用參考,最常見的就是函式模板引數:

template<typename T>
void func(T&& param); // param是一個通用參考

第二種情況是auto宣告符:

auto&& a = b;	//a是一個通用參考

以上兩種情況的共同之處在于都是型別推導,在func內部,param型別需要被推導,在auto宣告中,a的型別也需要被推導,而如果帶有&&而不需要推導,則就是普通的右值參考:

void func(A&& param);	// 沒有型別推導,param是一個右值參考
A&& a = b;		// 沒有型別推導,a是一個右值參考

由于參考必須初始化,通用參考也一樣,一個通用參考的初始值決定了其具體代表的是一個左值參考還是右值參考,如果初始值是一個左值,那么通用參考對應的就是左值參考,如果初始值是一個右值,那么通用參考對應的就是一個右值參考,

template<typename T>
void func(T&& param); // param是一個通用參考

int a = 1;
func(a);		// a是左值,T被推導為int&,引數param的型別是int&,是一個左值參考
func(5);		// 5是右值,T被推導為int,引數param的型別是int&&,是一個右值參考

需要注意的是,判斷一個參考是不是通用參考,型別推導是必要的,但是并不是型別推導就是通用參考,還需要看是不是準確的T&&,如:

template<typename T>
void func(std::vector<T>&& param); // param是一個右值參考

template<typename T>
void func(const T&& param); // param是一個右值參考

上述模板函式func被呼叫的時候,型別T也會被推導,但是引數param的型別并不是T&&,而是一個std::vector&&,因此param是一個右值參考而不是通用參考,即使多了一個const,那么param也不能成為一個通用參考,

理解std::move()

有了上述的知識基礎之后,C++中的move函式功能就很好理解了,std::move的主要作用是將一個左值/右值無條件的轉換為右值,但是函式本身并不移動任何東西,只是進行型別的轉換,那么這種轉換是如何做到的呢?我們來看下std::move具體實作的代碼:

template<class T>
typename remove_reference<T>::type&& move(T&& param)
{
    using returnType = typename remove_reference<T>::type&&;
    return static_cast<returnType>(param);
}

通過原始碼可以看到,std::move接受一個通用參考的引數,函式回傳一個&&表明std::move函式回傳的是一個右值參考,這里remove_reference表示移除型別T的參考部分,具體的實作可以參考檔案,即回傳結果是右值,在C++14中std::move的實作更加簡單:

template<typename T>
decltype(auto) move(T&& param)
{
    using returnType = remove_reference_t<T>&&;
    return static_cast<returnType>(param);
}

讓我們通過以下的代碼示例具體分析下std::move是如何作業的:

string s1("hello"),s2;
s2 = std::move(string("world")); // 從右值移動資料
s2 = std::move(s1);		 // 將左值轉換為右值 

在第一個賦值中,傳遞給move的實參是一個右值,當向一個右值參考傳遞一個右值時,推導的型別即被參考的型別,因此在std::move(string("world"))中:

  • T被推導為string
  • returnType為string
  • move的回傳型別為string&&
  • move的函式引數param的型別為string&&
    則函式std::move被推導為:
string&& move(string&& param)
{
	return static_cast<string&&>(param);
}

由于param已經時右值參考型別,因此實際上move函式什么也沒做,
在第二個賦值中,傳給std::move的引數是一個左值,則在std::move(s1)中:

  • T被推導為string&
  • returnType為string
  • move的回傳型別為string&&
  • move的函式引數param的型別為string&
    則函式std::move被推導為:
string&& move(string& param)
{
	return static_cast<string&&>(param);
}

可以看到引數param被static_cast轉換為sting&&,在C++中,從一個左值static_cast到一個右值參考時允許的
從以上的示例可以看到,不管傳入的是左值還是右值,最終move都會回傳一個右值,

理解std::forward()

std::forward與std::move實作的功能是類似的,只不過std::move總是無條件的將它的引數轉換為右值,而std::forward只有在滿足一定的條件下才會執行轉換,std::forward最常見的使用場景是一個模板函式,接受一個通用參考引數,并將其傳遞給另外的函式:

void Process(const A& lvalue);	// 處理左值
void Process(A&& rvalue);	// 處理右值

template<typename T>
void PrintAndProcess(T&& param)
{
    Print("Some Log");
    process(std::forward<T>(param))
}

現在考慮兩次對PrintAndProcess的呼叫,一次引數為左值,一次引數為右值

A a;
PrintAndProcess(a);		// 左值引數
PrintAndProcess(std::move(a));	// 右值引數

在PrintAndProcess函式內部,引數param被傳遞給process函式,process函式分別對左值和右值進行了多載,傳入PrintAndProcess左值引數時希望process左值版本被呼叫,傳入PrintAndProcess右值引數時,process右值版本被呼叫,但是前面我們提過,一個右值參考的變數,其本身時一個左值,因此無論傳給PrintAndProcess函式的實參時左值還是右值,最終呼叫process函式都是左值版本,為了解決這個問題,我們就需要一種機制:當傳入PrintAndProcess函式的實參是右值時,呼叫的時process的右值版本,這就是std::forward的使用場景:只把由右值初始化的引數,轉換為右值

那么std::forward如何知道param引數是被一個左值還是一個右值給初始化的呢?我們來看下std::forward實作的原始碼:

template<class T>
constexpr T&& forward(std::remove_reference_t<T>& arg) noexcept{
    // forward an lvalue as either an lvalue or an rvalue
    return (static_cast<T&&>(arg));
}

template<class T>
constexpr T&& forward(std::remove_reference_t<T>&& arg) noexcept{
    static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
        " substituting _Tp is an lvalue reference type");
    // forward an rvalue as an rvalue
    return (static_cast<T&&>(arg));
}

對于左值的轉發,首先通過獲取型別type,定義args為左值參考的左值變數,然后通過static_cast<T&&>進行強制轉換,這里T&&會發生參考折疊,當T被推導為左值參考時,則為T&& &,折疊為T&,當推導為右值參考時,則本身為T&&,forward回傳值與static_cast都為T&&,
對于右值的轉發不同于左值,只有當型別時右值時才進行static_cast轉換,arg為右值參考的左值變數,通過cast轉換為T&&,
對應到上述PrintAndProcess函式中我們進行分析:

  • 當PrintAndProcess(a),傳入的為左值A時,T被推導為A&,std::forward回傳值和static_cast被推導為A& &&,折疊為A&,回傳一個左值,
  • 當PrintAndProcess(std::move(a)),傳入為右值時,T被推導為A,在std::forward回傳值和static_cast被推導為T&&,回傳一個右值,

std::move 和 std::forward對比

  • std::move執行到右值的無條件轉換,就其本身而言,它沒有move任何東西,
  • std::forward只有在它的引數系結到一個右值上的時候,它才轉換它的引數到一個右值,
  • std::move和std::forward只不過就是執行型別轉換的兩個函式;std::move沒有move任何東西,std::forward沒有轉發任何東西,在運行期,它們沒有做任何事情,它們沒有產生需要執行的代碼,一個byte都沒有,
  • std::forward()不僅可以保持左值或者右值不變,同時還可以保持const、Lreference、Rreference、validate等屬性不變;

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/549552.html

標籤:其他

上一篇:陣列的演算法

下一篇:Golang與Java全方位對比總結

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more