C++深入右值引用之移動語義與完美轉發(fā)(最新推薦)
1. 左值與右值
左值和右值不是新概念,C++98就有,但C++11賦予了它們更重要的地位。
- 左值:可以取地址的表達式,有持久狀態(tài)。變量、解引用的指針、數(shù)組元素都是左值。
- 右值:不能取地址。字面量、臨時對象、表達式求值的中間結果。
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10; // *p 是左值
string s("hello");
s[0] = 'x'; // s[0] 是左值
10; // 右值
x + y; // 右值
string("hello"); // 右值
// cout << &10; // 錯誤:右值不能取地址C++11進一步細化了:右值分為純右值(字面量、臨時對象等)和將亡值(move返回的右值引用)。泛左值包含左值和將亡值。這些概念不必死記,關鍵是記住核心區(qū)別:能否取地址。
2. 左值引用與右值引用
左值引用(T&)給左值取別名,右值引用(T&&)給右值取別名。
int& r1 = b; // 左值引用綁定左值
int&& rr1 = 10; // 右值引用綁定右值
double&& rr2 = x + y;
string&& rr3 = string("hello");幾個規(guī)則:
- 左值引用不能直接綁定右值,但const左值引用可以。
- 右值引用不能直接綁定左值,但可以通過
std::move轉換。
const int& cr = 10; // OK int&& rr4 = move(b); // OK,move后的b仍可使用但資源已被取走
move()本質上是一個強制類型轉換:static_cast<remove_reference_t<T>&&>(arg)。它本身不移動任何東西,只是給了你一個右值引用,真正的動作由移動構造函數(shù)或移動賦值完成。
有一個容易困惑的點:右值引用變量本身的表達式屬性是左值。因為它有名字、可以取地址。
int&& rr = 10; cout << &rr << endl; // 可以取地址,rr是左值 int& lr = rr; // OK,rr作為左值可以綁定左值引用 // int&& rr2 = rr; // 錯誤,rr是左值,不能直接綁定右值引用 int&& rr2 = move(rr); // OK
這個設計看似別扭,但恰恰是移動語義能正常工作的基礎——后面會看到。
3. 移動構造與移動賦值
左值引用已經(jīng)在函數(shù)傳參和返回值中減少了大部分拷貝,但有一種場景它搞不定:返回局部對象。
string addStrings(string num1, string num2) {
string str;
// ... 構建str ...
return str; // str是局部對象,函數(shù)結束就銷毀,不能傳引用返回
}C++98被迫拷貝,C++11通過移動語義解決了這個問題:既然str馬上就要銷毀,把它的資源“偷”過來就行,沒必要深拷貝。
移動構造和移動賦值接受一個右值引用參數(shù),核心操作是交換資源,而不是復制。
namespace demo {
class string {
public:
string(const char* str = "")
: _size(strlen(str)), _capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 拷貝構造
string(const string& s) {
// 深拷貝...
}
// 移動構造
string(string&& s) {
swap(s); // 直接把s的資源換到自己身上
}
// 拷貝賦值
string& operator=(const string& s) {
// 深拷貝...
}
// 移動賦值
string& operator=(string&& s) {
swap(s);
return *this;
}
void swap(string& s) {
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
~string() { delete[] _str; }
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}使用效果:
demo::string s1("hello");
demo::string s2 = s1; // 拷貝構造
demo::string s3 = demo::string("world"); // 移動構造(臨時對象)
demo::string s4 = move(s1); // 移動構造(顯式move)編譯器還會進一步優(yōu)化。在VS2022的release模式下,demo::string s3 = demo::string("world");可能直接被優(yōu)化成一次原地構造,移動構造都不會調用——這就是返回值優(yōu)化(RVO)。
返回值場景中,如果提供了移動構造,沒有優(yōu)化時編譯器會優(yōu)先選擇移動而不是拷貝,顯著提高效率。
4. 移動語義在容器中的應用
C++11之后,STL容器的push_back、insert都增加了右值引用版本:
void push_back(const T& x); // 左值版本,內(nèi)部拷貝 void push_back(T&& x); // 右值版本,內(nèi)部移動
當傳入左值,走拷貝;傳入右值,走移動。
list<demo::string> lt;
demo::string s1("hello");
lt.push_back(s1); // 拷貝
lt.push_back(move(s1)); // 移動
lt.push_back("world"); // 移動(臨時string)
lt.push_back(demo::string("world")); // 移動自定義容器也可以實現(xiàn)相應的重載,內(nèi)部在節(jié)點構造時用move把參數(shù)轉成右值。
5. 完美轉發(fā)與引用折疊
考慮一個模板函數(shù),想要把參數(shù)原樣轉發(fā)給另一個函數(shù):
template<class T>
void relay(T&& t) {
func(t); // t是左值,永遠調用func的左值版本,即使傳進來的是右值
}問題在于:t作為右值引用變量,表達式屬性是左值,所以func(t)調不到右值重載版本。要保留參數(shù)的原始值類別,需要完美轉發(fā):
template<class T>
void relay(T&& t) {
func(forward<T>(t));
}forward的實現(xiàn)依賴于引用折疊規(guī)則。C++不允許直接定義引用的引用(int& &),但在模板和類型推導中可以出現(xiàn)。規(guī)則只有一條:右值引用的右值引用折疊成右值引用,其余全部折疊成左值引用。
typedef int& lref; typedef int&& rref; lref& r1 = n; // int& & → int& lref&& r2 = n; // int& && → int& rref& r3 = n; // int&& & → int& rref&& r4 = 1; // int&& && → int&&
在relay中,T的推導結果配合引用折疊,實現(xiàn)了“左值傳左值,右值傳右值”:
- 實參是左值
int a→T推導為int&→T&&折疊為int& - 實參是右值
10→T推導為int→T&&為int&&
forward內(nèi)部也是通過static_cast<T&&>實現(xiàn)的,配合引用折疊,左值情況返回左值引用,右值情況返回右值引用。這樣就把參數(shù)的原始屬性一路傳下去了。
6. 默認移動構造與移動賦值
編譯器在特定條件下會自動生成移動構造和移動賦值:
- 如果用戶沒有定義拷貝構造、拷貝賦值、析構函數(shù)中的任何一個,編譯器會嘗試自動生成移動構造和移動賦值。
- 對于內(nèi)置類型成員,逐字節(jié)拷貝;對于自定義類型成員,如果能移動就移動,否則拷貝。
一旦你提供了移動構造或移動賦值,編譯器就不會自動生成拷貝構造和拷貝賦值。所以如果你想兩者都有,需要自己顯式聲明,或者用= default:
class Person {
public:
Person(const Person&) = default; // 顯式要拷貝
Person(Person&&) = default; // 顯式要移動
};也可以用= delete禁止某個函數(shù)。
移動語義是C++11最核心的特性之一。它讓“快”從“避免拷貝”的編碼技巧,變成了語言級別的優(yōu)化路徑。
到此這篇關于C++深入右值引用之移動語義與完美轉發(fā)(最新推薦)的文章就介紹到這了,更多相關C++右值引用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
淺析C語言中strtol()函數(shù)與strtoul()函數(shù)的用法
這篇文章主要介紹了淺析C語言中strtol()函數(shù)與strtoul()函數(shù)的用法,注意其將字符串轉換成long型的區(qū)別,需要的朋友可以參考下2015-08-08
C語言中strlen() strcpy() strcat() strcmp()函數(shù)的實現(xiàn)方法
這篇文章主要介紹了C語言中strlen() strcpy() strcat() strcmp()函數(shù)的實現(xiàn)方法,需要的朋友可以參考下2017-08-08

