MordernC++之左值(引用)與右值(引用)

来源:https://www.cnblogs.com/zutterhao/archive/2023/04/09/17299837.html
-Advertisement-
Play Games

左值與右值 C++中左值與右值的概念是從C中繼承而來,一種簡單的定義是左值能夠出現再表達式的左邊或者右邊,而右值只能出現在表達式的右邊。 int a = 5; // a是左值,5是右值 int b = a; // b是左值,a也是左值 int c = a + b; // c是左值,a + b是右值 ...


左值與右值

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等屬性不變;

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 之前一直以為固態硬碟各方面都比機械硬碟性能高,所以首選固態硬碟,直到看了極客時間-深入淺出電腦組成原理中硬碟相關章節的內容,才發現固態硬碟原來是有缺點的,所以這裡來做一個總結。 機械硬碟(HDD) 機械硬碟由以下幾個部分組成: 盤面:盤面(碟片)上有一層磁性塗層,數據就是存儲在這個磁性的塗層上,一 ...
  • 網路分層結構 電腦網路體系大致分為三種,OSI七層模型、TCP/IP四層模型和五層模型。一般面試的時候考察比較多的是五層模型。最全面的Java面試網站 五層模型:應用層、傳輸層、網路層、數據鏈路層、物理層。 應用層:為應用程式提供交互服務。在互聯網中的應用層協議很多,如功能變數名稱系統DNS、HTTP協議 ...
  • 當今的軟體開發需要使用許多不同的工具和技術來確保代碼質量和穩定性。PMD是一個流行的靜態代碼分析工具,可以幫助開發者在編譯代碼之前發現潛在的問題。在本文中,我們將討論如何在Gradle中使用PMD,並介紹一些最佳實踐。 什麼是PMD? PMD是一個用於Java代碼的靜態代碼分析工具。它可以幫助開發者 ...
  • demo軟體園每日更新資源,請看到最後就能獲取你想要的: 1.完善版手游導航源碼app軟體 APP手機軟體 應用商城下載類網站佈局規整,利於用戶體驗 瀏覽網站看到一款帶後臺的app軟體手游類源碼,後臺功能強大,界面美觀,適用於app軟體,手機軟體下載,手游類導航網, 其他行業也可以把數據刪掉,添加自 ...
  • 文章目錄一、前言一、方式1:spring 官方創建 springboot項目1、打開線上的 spring initializr2、選擇項目的語言、版本、依賴等3、 解壓源碼包,並使用IDEA打開4、測試介面二、方式2:社區idea安裝Spring插件1、添加插件三、方式3:(麻煩)手動maven 創 ...
  • 一、簡介 本文博主給大家講解如何在自己開源的電商項目newbee-mall-pro中應用協同過濾演算法來達到給用戶更好的購物體驗效果。 newbee-mall-pro項目地址: 源碼地址:https://github.com/wayn111/newbee-mall-pro 線上地址:http://12 ...
  • 原文鏈接: Go 語言切片是如何擴容的? 在 Go 語言中,有一個很常用的數據結構,那就是切片(Slice)。 切片是一個擁有相同類型元素的可變長度的序列,它是基於數組類型做的一層封裝。它非常靈活,支持自動擴容。 切片是一種引用類型,它有三個屬性:指針,長度和容量。 底層源碼定義如下: type s ...
  • 本文針對Golang與Java的基礎語法、結構體函數、異常處理、併發編程及垃圾回收、資源消耗等各方面的差異進行對比總結,有不准確、不到位的地方還請大家不吝賜教。 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...