右值引用(rvalue reference)是 C++11 為了實(shí)現(xiàn)移動(dòng)語意(move semantic)和完美轉(zhuǎn)發(fā)(perfect forwarding)而提出來的。
右值引用灶平,簡單說就是綁定在右值上的引用伺通。右值的內(nèi)容可以直接移動(dòng)(move)給左值對(duì)象,而不需要進(jìn)行開銷較大的深拷貝(deep copy)逢享。
移動(dòng)語義
下面這個(gè)例子:
-
v2 = v1
調(diào)用的是拷貝賦值操作符罐监,v2 復(fù)制了 v1 的內(nèi)容 —— 復(fù)制語義。 -
v3 = std::move(v1)
調(diào)用的是移動(dòng)賦值操作符拼苍,將 v1 的內(nèi)容移動(dòng)給 v3 —— 移動(dòng)語義笑诅。
std::vector<int> v1{1, 2, 3, 4, 5};
std::vector<int> v2;
std::vector<int> v3;
v2 = v1;
std::cout << v1.size() << std::endl; // 輸出 5
std::cout << v2.size() << std::endl; // 輸出 5
v3 = std::move(v1); // move
std::cout << v1.size() << std::endl; // 輸出0
std::cout << v3.size() << std::endl; // 輸出 5
為了實(shí)現(xiàn)移動(dòng)語意,C++ 增加了與拷貝構(gòu)造函數(shù)(copy constructor)和拷貝賦值操作符(copy assignment operator)對(duì)應(yīng)的移動(dòng)構(gòu)造函數(shù)(move constructor)和移動(dòng)賦值操作符(move assignment operator)疮鲫,通過函數(shù)重載機(jī)制來確定應(yīng)該調(diào)用拷貝語意還是移動(dòng)語意(參數(shù)是左值引用就調(diào)用拷貝語意吆你;參數(shù)是右值引用就調(diào)用移動(dòng)語意)。
再來看一個(gè)簡單的例子:
#include <iostream>
#include <string>
#include <vector>
class Foo {
public:
// 默認(rèn)構(gòu)造函數(shù)
Foo() { std::cout << "Default Constructor: " << Info() << std::endl; }
// 自定義構(gòu)造函數(shù)
Foo(const std::string& s, const std::vector<int>& v) : s_(s), v_(v) {
std::cout << "User-Defined Constructor: " << Info() << std::endl;
}
// 析構(gòu)函數(shù)
~Foo() { std::cout << "Destructor: " << Info() << std::endl; }
// 拷貝構(gòu)造函數(shù)
Foo(const Foo& f) : s_(f.s_), v_(f.v_) {
std::cout << "Copy Constructor: " << Info() << std::endl;
}
// 拷貝賦值操作符
Foo& operator=(const Foo& f) {
s_ = f.s_;
v_ = f.v_;
std::cout << "Copy Assignment: " << Info() << std::endl;
return *this;
}
// 移動(dòng)構(gòu)造函數(shù)
Foo(Foo&& f) : s_(std::move(f.s_)), v_(std::move(f.v_)) {
std::cout << "Move Constructor: " << Info() << std::endl;
}
// 移動(dòng)賦值操作符
Foo& operator=(Foo&& f) {
s_ = std::move(f.s_);
v_ = std::move(f.v_);
std::cout << "Move Assignment: " << Info() << std::endl;
return *this;
}
std::string Info() {
return "{" + (s_.empty() ? "'empty'" : s_) + ", " +
std::to_string(v_.size()) + "}";
}
private:
std::string s_;
std::vector<int> v_;
};
int main() {
std::vector<int> v(1024);
std::cout << "================ Copy =======================" << std::endl;
Foo cf1("hello", v);
Foo cf2(cf1); // 調(diào)用拷貝構(gòu)造函數(shù)
Foo cf3;
cf3 = cf2; // 調(diào)用拷貝賦值操作符
std::cout << "================ Move =========================" << std::endl;
Foo f1("hello", v);
Foo f2(std::move(f1)); // 調(diào)用移動(dòng)構(gòu)造函數(shù)
Foo f3;
f3 = std::move(f2); // 調(diào)用移動(dòng)賦值操作符
return 0;
}
簡單封裝了一個(gè)類 Foo俊犯,重點(diǎn)是實(shí)現(xiàn):
- 拷貝語意:拷貝構(gòu)造函數(shù)
Foo(const Foo&)
妇多、拷貝賦值操作符Foo& operator=(const Foo&)
。 - 移動(dòng)語意:移動(dòng)構(gòu)造函數(shù)
Foo(Foo&&)
燕侠、移動(dòng)賦值操作符Foo& operator=(Foo&&)
者祖。
拷貝語意相信大部分人都比較熟悉了,也比較好理解绢彤。在這個(gè)例子中七问,每次都會(huì)拷貝 s_
和 v_
兩個(gè)成員,最后 cf1茫舶、cf2械巡、cf3 三個(gè)對(duì)象的內(nèi)容都是一樣的。
每次執(zhí)行移動(dòng)語意,是分別調(diào)用 s_
和 v_
的移動(dòng)語意函數(shù)——理論上只需要對(duì)內(nèi)部指針進(jìn)行修改讥耗,所以效率較高有勾。執(zhí)行移動(dòng)語意的代碼片段了出現(xiàn)了一個(gè)標(biāo)準(zhǔn)庫中的函數(shù) std::move
—— 它可以將參數(shù)強(qiáng)制轉(zhuǎn)換成一個(gè)右值。本質(zhì)上是告訴編譯器古程,我想要 move 這個(gè)參數(shù)——最終能不能 move 是另一回事——可能對(duì)應(yīng)的類型沒有實(shí)現(xiàn)移動(dòng)語意蔼卡,可能參數(shù)是 const 的。
有一些場景可能拿到的值直接就是右值挣磨,不需要通過 std::move
強(qiáng)制轉(zhuǎn)換雇逞,比如:
Foo GetFoo() {
return Foo("GetFoo", std::vector<int>(11));
}
....
Foo f3("world", v3);
....
f3 = GetFoo(); // GetFoo 返回的是一個(gè)右值,調(diào)用移動(dòng)賦值操作符
完美轉(zhuǎn)發(fā)
C++ 通過了一個(gè)叫 std::forward
的函數(shù)模板來實(shí)現(xiàn)完美轉(zhuǎn)發(fā)趋急。這里直接使用 Effective Modern C++ 中的例子作為說明喝峦。在前面的例子上势誊,我們?cè)黾尤缦碌拇a:
// 接受一個(gè) const 左值引用
void Process(const Foo& f) {
std::cout << "lvalue reference" << std::endl;
// ...
}
// 接受一個(gè)右值引用
void Process(Foo&& f) {
std::cout << "rvalue reference" << std::endl;
// ...
}
template <typename T>
void LogAndProcessNotForward(T&& a) {
std::cout << a.Info() << std::endl;
Process(a);
}
template <typename T>
void LogAndProcessWithForward(T&& a) {
std::cout << a.Info() << std::endl;
Process(std::forward<T>(a));
}
LogAndProcessNotForward(f3); // 輸出 lvalue reference
LogAndProcessNotForward(std::move(f3)); // 輸出 lvalue reference
LogAndProcessWithForward(f3); // 輸出 lvalue reference
LogAndProcessWithForward(std::move(f3)); // 輸出 rvalue reference
-
LogAndProcessNotForward(f3);
和LogAndProcessWithForward(f3);
都輸出 "lvalue reference"呜达,這一點(diǎn)都不意外,因?yàn)?f3 本來就是一個(gè)左值粟耻。 -
LogAndProcessNotForward(std::move(f3));
輸出 "lvalue reference" 是因?yàn)殡m然參數(shù) a 綁定到一個(gè)右值查近,但是參數(shù) a 本身是一個(gè)左值。 -
LogAndProcessWithForward(std::move(f3));
使用了std::forward
對(duì)參數(shù)進(jìn)行轉(zhuǎn)發(fā)挤忙,std::forward 的作用就是:當(dāng)參數(shù)是綁定到一個(gè)右值時(shí)霜威,就將參數(shù)轉(zhuǎn)換成一個(gè)右值。
左值册烈?右值戈泼?
到底什么時(shí)候是左值?什么時(shí)候是右值赏僧?是不是有點(diǎn)混亂大猛?
在 C++ 中,每個(gè)表達(dá)式(expression)都有兩個(gè)特性:
- has identity? —— 是否有唯一標(biāo)識(shí)淀零,比如地址挽绩、指針。有唯一標(biāo)識(shí)的表達(dá)式在 C++ 中被稱為 glvalue(generalized lvalue)驾中。
- can be moved from? —— 是否可以安全地移動(dòng)(編譯器)唉堪。可以安全地移動(dòng)的表達(dá)式在 C++ 中被成為 rvalue肩民。
根據(jù)這兩個(gè)特性唠亚,可以將表達(dá)式分成 4 類:
- has identity and cannot be moved from - 這類表達(dá)式在 C++ 中被稱為 lvalue。
- has identity and can be moved from - 這類表達(dá)式在 C++ 中被成為 xvalue(expiring value)持痰。
- does not have identity and can be moved from - 這類表達(dá)式在 C++ 中被成為 prvalue(pure rvalue)灶搜。
- does not have identity and cannot be moved -C++ 中不存在這類表達(dá)式。
簡單總結(jié)一下這些 value categories 之間的關(guān)系:
- 可以移動(dòng)的值都叫 rvalue,包括 xvalue 和 prvalue占调。
- 有唯一標(biāo)識(shí)的值都叫 glvalue暂题,包括 lvalue 和 xvalue。
- std::move 的作用就是將一個(gè) lvalue 轉(zhuǎn)換成 xvalue究珊。
這些概念其實(shí)有點(diǎn)繞薪者。不過就算不是特別清楚這些概念,也不影響我們對(duì)移動(dòng)語義的利用剿涮。