主頁 > 後端開發 > 【重學C++】04 | 說透C++右值參考、移動語意、完美轉發(上)

【重學C++】04 | 說透C++右值參考、移動語意、完美轉發(上)

2023-05-23 11:05:29 後端開發

文章首發

【重學C++】04 | 說透C++右值參考、移動語意、完美轉發(上)

引言

大家好,我是只講技術干貨的會玩code,今天是【重學C++】的第四講,在前面《03 | 手擼C++智能指標實戰教程》中,我們或多或少接觸了右值參考和移動的一些用法,

右值參考是 C++11 標準中一個很重要的特性,第一次接觸時,可能會很亂,不清楚它們的目的是什么或者它們解決了什么問題,接下來兩節課,我們詳細講講右值參考及其相關應用,內容很干,注意收藏!

左值 vs 右值

簡單來說,左值是指可以使用&符號獲取到記憶體地址的運算式,一般出現在賦值陳述句的左邊,比如變數、陣列元素和指標等,

int i = 42;
i = 43; // ok, i是一個左值
int* p = &i; // ok, i是一個左值,可以通過&符號獲取記憶體地址

int& lfoo() { // 回傳了一個參考,所以lfoo()回傳值是一個左值
	int a = 1;
	return a; 
};
lfoo() = 42; // ok, lfoo() 是一個左值
int* p1 = &lfoo(); // ok, lfoo()是一個左值

相反,右值是指無法獲取到記憶體地址的表達是,一般出現在賦值陳述句的右邊,常見的有字面值常量、運算式結果、臨時物件等,

int rfoo() { // 回傳了一個int型別的臨時物件,所以rfoo()回傳值是一個右值
	return 5;
};

int j = 0;
j = 42; // ok, 42是一個右值
j = rfoo(); // ok, rfoo()是右值
int* p2 = &rfoo(); // error, rfoo()是右值,無法獲取記憶體地址

左值參考 vs 右值參考

C++中的參考是一種別名,可以通過一個變數名訪問另一個變數的值,
image.png

上圖中,變數a和變數b指向同一塊記憶體地址,也可以說變數a是變數b的別名,

在C++中,參考分為左值參考和右值參考兩種型別,左值參考是指對左值進行參考的參考型別,通常使用&符號定義;右值參考是指對右值進行參考的參考型別,通常使用&&符號定義,

class X {...};
// 接收一個左值參考
void foo(X& x);
// 接收一個右值參考
void foo(X&& x);

X x;
foo(x); // 傳入引數為左值,呼叫foo(X&);

X bar();
foo(bar()); // 傳入引數為右值,呼叫foo(X&&);

所以,通過多載左值參考和右值參考兩種函式版本,滿足在傳入左值和右值時觸發不同的函式分支,

值得注意的是,void foo(const X& x);同時接受左值和右值傳參,

void foo(const X& x);
X x;
foo(x); // ok, foo(const X& x)能夠接收左值傳參

X bar();
foo(bar()); // ok, foo(const X& x)能夠接收右值傳參

// 新增右值參考版本
void foo(X&& x);
foo(bar()); // ok, 精準匹配呼叫foo(X&& x)

到此,我們先簡單對右值和右值參考做個小結:

  1. 像字面值常量、運算式結果、臨時物件等這類無法通過&符號獲取變數記憶體地址的,稱為右值,
  2. 右值參考是一種參考型別,表示對右值進行參考,通常使用&&符號定義,

右值參考主要解決一下兩個問題:

  1. 實作移動語意
  2. 實作完美轉發

這一節我們先詳細講講右值是如何實作移動效果的,以及相關的注意事項,完美轉發篇幅有點多,我們留到下節講,

復制 vs 移動

假設有一個自定義類X,該類包含一個指標成員變數,該指標指向另一個自定義類物件,假設O占用了很大記憶體,創建/復制O物件需要較大成本,

class O {
public:
	O() {
		std::cout << "call o constructor" << std::endl;
	};
	O(const O& rhs) {
		std::cout << "call o copy constructor." << std::endl;
	}
};

class X {
public:
	O* o_p;
	X() {
		o_p = new O();
	}
	~X() {
		delete o_p;
	}
};

X 對應的拷貝賦值函式如下:

X& X::operator=(X const & rhs) {
	// 根據rhs.o_p生成的一個新的O物件資源
	O* tmp_p = new O(*rhs.o_p);
	// 回收x當前的o_p;
	delete this->o_p;
	// 將tmp_p 賦值給 this.o_p;
	this->o_p = tmp_p;
	return *this;
}

假設對X有以下使用場景:

X x1;
X x2;
x1 = x2;

上述代碼輸出:

call o constructor
call o constructor
call o copy constructor

x1x2初始化時,都會執行new O(), 所以會呼叫兩次O的建構式;執行x1=x2時,會呼叫一次O的拷貝建構式,根據x2.o_p復制一個新的O物件,

由于x2在后續代碼中可能還會被使用,所以為了避免影響x2,在賦值時呼叫O的拷貝建構式復制一個新的O物件給x1在這種場景下是沒問題的,

但在某些場景下,這種拷貝顯得比較多余:

X foo() {
	return X();
};

X x1;
x1 = foo();

代碼輸出與之前一樣:

call o constructor
call o constructor
call o copy constructor

在這個場景下,foo()創建的那個臨時X物件在后續代碼是不會被用到的,所以我們不需要擔心賦值函式中會不會影響到那個臨時X物件,沒必要去復制一個新的O物件給x1

更高效的做法,是直接使用swap交換臨時X物件的o_px1.o_p,這樣做有兩個好處:1. 不用呼叫耗時的O拷貝建構式,提高效率;2. 交換后,臨時X物件擁有之前x1.o_p指向的資源,在析構時能自動回收,避免記憶體泄漏,

這種避免高昂的復制成本,而直接將資源從一個物件"移動"到另外一個物件的行為,就是C++的移動語意,

哪些場景適用移動操作呢?無法獲取記憶體地址的右值就很合適,我們不需要擔心后續的代碼會用到該右值,

最后,我們看下移動版本的賦值函式

X& operator=(X&& rhs) noexcept {
	std::swap(this->o_p, rhs.o_p);
	return *this;
};

看下使用效果:

X x1;
x1 = foo();

輸出結果:

call o constructor
call o constructor

右值參考一定是右值嗎?

假設我們有以下代碼:

class X {
public:
	// 復制版本的賦值函式
	X& operator=(const X& rhs);

	// 移動版本的賦值函式
	X& operator=(X&& rhs) noexcept;
};

void foo(X&& x) {
	X x1;
	x1 = x;
}

X多載了復制版本和移動版本的賦值函式,現在問題是:x1=x這個賦值操作呼叫的是X& operator=(const X& rhs)還是 X& operator=(X&& rhs)
針對這種情況,C++給出了相關的標準:

Things that are declared as rvalue reference can be lvalues or rvalues. The distinguishing criterion is: if it has a name, then it is an lvalue. Otherwise, it is an rvalue.

也就是說,只要一個右值參考有名稱,那對應的變數就是一個左值,否則,就是右值,

回到上面的例子,函式foo的入參雖然是右值參考,但有變數名x,所以x是一個左值,所以operator=(const X& rhs)最侄訓被呼叫,

再給一個沒有名字的右值參考的例子

X bar();
// 呼叫X& operator=(X&& rhs),因為bar()回傳的X物件沒有關聯到一個變數名上
X x = bar();

這么設計的原因也挺好理解,再改下foo函式的邏輯:

void foo(X&& x) {
	X x1;
	x1 = x;
	...
	std::cout << *(x.inner_ptr) << std::endl;
}

我們并不能保證在foo函式的后續邏輯中不會訪問到x的資源,所以這種情況下如果呼叫的是移動版本的賦值函式,x的內部資源在完成賦值后就亂了,無法保證后續的正常訪問,

std::move

反過來想,如果我們明確知道在x1=x后,不會再訪問到x,那有沒有辦法強制走移動賦值函式呢?

C++提供了std::move函式,這個函式做的作業很簡單: 通過隱藏掉入參的名字,回傳對應的右值,

X bar();
X x1
// ok. std::move(x1)回傳右值,呼叫移動賦值函式
X x2 = std::move(x1);
// ok. std::move(bar())與 bar()效果相同,回傳右值,呼叫移動賦值函式
X x3 = std::move(bar());

最后,用一個容易犯錯的例子結束這一環節

class Base {
public:
	// 拷貝建構式
	Base(const Base& rhs);
	// 移動建構式
	Base(Base&& rhs) noexcept;
};

class Derived : Base {
public:
	Derived(Derived&& rhs)
	// wrong. rhs是左值,會呼叫到 Base(const Base& rhs).
	// 需要修改為Base(std::move(rhs))
	: Base(rhs) noexcept {
		...
	}
}

回傳值優化

依照慣例,還是先給出類X的定義

class X {
public:
	// 建構式
	X() {
		std::cout << "call x constructor" <<std::endl;
	};
	// 拷貝建構式
	X(const X& rhs) {
		std::cout << "call x copy constructor" << std::endl;
	};
	// 移動建構式
	X(X&& rhs) noexcept {
		std::cout << "call x move constructor" << std::endl
	};
}

大家先思考下以下兩個函式哪個性能比較高?

X foo() {
  X x;
  return x;
};

X bar() {
  X x;
  return std::move(x);
}

很多讀者可能會覺得foo需要一次復制行為:從x復制到回傳值;bar由于使用了std::move,滿足移動條件,所以觸發的是移動建構式:從x移動到回傳值,復制成本 > 移動成本,所以bar性能更好,

實際效果與上面的推論相反,bar中使用std::move反倒多余了,現代C++編譯器會有回傳值優化,換句話說,編譯器將直接在foo回傳值的位置構造x物件,而不是在本地構造x然后將其復制出去,很明顯,這比在本地構造后移動效率更快,

以下是foobar的輸出:

// foo
call x constructor

// bar
call x constructor
call x move constructor

移動需要保證例外安全

細心的讀者可能已經發現了,在前面的幾個小節中,移動構造/賦值函式我都在函式簽名中加了關鍵字noexcept,這是向呼叫者表明,我們的移動函式不會拋出例外,

這點對于移動函式很重要,因為移動操作會對右值造成破壞,如果移動函式中發生了例外,可能會對程式造成不可逆的錯誤,以下面為例

class X {
public:
	int* int_p;
	O* o_p;

	X(X&& rhs) {
		std::swap(int_p, rhs.int_p);
		...
		其他業務操作
		...
		std::swap(o_p, rhs.o_p);
	}
}

如果在「其他業務操作」中發生了例外,不僅會影響到本次構造,rhs內部也已經被破壞了,后續無法重試構造,所以,除非明確標識noexcept,C++在很多場景下會慎用移動構造,

比較經典的場景是std::vector 擴縮容,當vector由于push_backinsertreserveresize 等函式導致記憶體重分配時,如果元素提供了一個noexcept的移動建構式,vector會呼叫該移動建構式將元素移動到新的記憶體區域;否則,則會呼叫拷貝建構式,將元素復制過去,

總結

今天我們主要學了C++中右值參考的相關概念和應用場景,并花了很大篇幅講解移動語意及其相關實作,

右值參考主要解決實作移動語意和完美轉發的問題,我們下節接著講解右值是如何實作完美轉發,歡迎關注,及時收到推送~

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

標籤:C++

上一篇:關于執行緒的快取重繪

下一篇:返回列表

標籤雲
其他(159521) Python(38162) JavaScript(25443) Java(18096) C(15231) 區塊鏈(8267) C#(7972) AI(7469) 爪哇(7425) MySQL(7207) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5871) 数组(5741) R(5409) Linux(5340) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4575) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2433) ASP.NET(2403) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1976) 功能(1967) Web開發(1951) HtmlCss(1940) C++(1920) python-3.x(1918) 弹簧靴(1913) xml(1889) PostgreSQL(1878) .NETCore(1861) 谷歌表格(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
最新发布
  • 【重學C++】04 | 說透C++右值參考、移動語意、完美轉發(上)

    ## 文章首發 [【重學C++】04 | 說透C++右值參考、移動語意、完美轉發(上)](https://mp.weixin.qq.com/s/35Jbt-vroWhxTk0SSyhgSQ) ## 引言 大家好,我是只講技術干貨的會玩code,今天是【重學C++】的第四講,在前面《[03 | 手擼C ......

    uj5u.com 2023-05-23 11:05:29 more
  • 關于執行緒的快取重繪

    今天又是摸魚的一天,在群里閑聊的時候突然有位群友題了個問題: ![](https://img2023.cnblogs.com/blog/2696704/202305/2696704-20230522233309409-1620806525.png) 群友們反應很快,一下子就解決了沒有加關鍵字vola ......

    uj5u.com 2023-05-23 08:32:03 more
  • Java中的三元運算,以后用得到!

    # 前言 Java 中的三元運算,平時也叫做三目運算,大家了解嗎?下面就詳細介紹一下,以后在專案編程中用得到。 # 一、Java運算子 在最底層,Java 中的資料是通過使用運算子來操作的。運算子是一種特殊的符號,用來表示資料的運算、賦值和比較等等。每一種編程語言都有運算子,在 Java 中運算子可 ......

    uj5u.com 2023-05-23 08:31:57 more
  • Java設計模式-組合模式

    # 簡介 在軟體設計中,設計模式是一種被廣泛接受和應用的經驗總結,旨在解決常見問題并提供可復用的解決方案。 組合模式是一種結構型設計模式,它允許將物件組合成樹形結構以表示“部分-整體”的層次結構。這種模式能夠使客戶端以一致的方式處理單個物件和物件集合,將物件的組合與物件的使用具有一致性。 與其他設計 ......

    uj5u.com 2023-05-23 08:31:53 more
  • Stream流體系

    視頻地址 # 1 Stream流概述 - 目的:簡化集合和陣列操作的API,結合了Lambda運算式。 - Stream流式思想的核心: 1. 先得到集合或者陣列的Stream流(就是一根傳送帶) 2. 把元素放上去 3. 用這個Stream流簡化的API來方便的操作元素 # 2 Stream流獲取 ......

    uj5u.com 2023-05-23 08:31:40 more
  • python呼叫父類方法的三種方式(super呼叫和父類名呼叫)

    ### 子類呼叫父類的方法的三種方式: - 父類名.方法名(self) - super(子類名,self).父類方法名() - super().父類方法名 注意:super()通過子類呼叫當前父類的方法,super默認會呼叫第一個父類的方法(適用于單繼承的多層繼承 如下代碼: ```python # ......

    uj5u.com 2023-05-23 08:31:31 more
  • 用Python將女朋友的照片做成壁紙軟體,實作桌面壁紙自動更換!

    話說兄弟們,女朋友生氣了都是怎么哄的? 不會吧不會吧,不會有人還是單身狗吧! 算了,還是回到正題吧,再說我要挨打了~ 今天咱們來交流一下程式員是怎么哄女朋友的,話不多說直接開始! 準備作業 1、環境 首先我們準備好環境和編輯器,我使用的是: Python 3.8 解釋器 Pycharm 編輯器 2、 ......

    uj5u.com 2023-05-23 08:26:16 more
  • 如何通過Java代碼將 PDF檔案轉為 HTML格式

    雖然PDF檔案適合用于列印和發布,但不適合所有型別的檔案。例如,包含復雜圖表和圖形的檔案可能無法在PDF中呈現得很好。但是HTML檔案可以在任何可運行瀏覽器的計算機上進行閱讀并顯示。并且HTML還具有占用服務器資源較小,便于搜索引擎收錄的特點。那么今天這篇文章就將展示如何通過Java應用程式將PDF ......

    uj5u.com 2023-05-23 08:21:05 more
  • Python豎版大屏 | 用pyecharts開發可視化的奇妙探索!

    你好!我是[@馬哥python說](https://www.zhihu.com/people/13273183132),一枚10年程式猿👨🏻?💻,正在試錯用pyecharts開發可視化大屏的非常規排版。 以下,我用8種ThemeType展示的同一個可視化資料大屏。 **1、SHINE主題** ......

    uj5u.com 2023-05-23 08:15:32 more
  • Java網路編程----通過實作簡易聊天工具來聊聊NIO

    前文我們說過了BIO,今天我們聊聊NIO。NIO 是什么?NIO官方解釋它為New lO,由于其特性我們也稱之為,Non-Blocking IO。這是jdk1.4之后新增的一套IO標準。為什么要用NIO呢?我們再簡單回顧下BIO:阻塞式IO,原理很簡單,其實就是多個端點與服務端進行通信時,每個客戶端 ......

    uj5u.com 2023-05-23 08:10:11 more