右值引用:移動語義和完美轉發(fā)
指針成員與拷貝構造
#include <iostream>
using namespace std;
class HasPtrMem {
public:
HasPtrMem(): d(new int(0)) {}
~HasPtrMem() { delete d; }
int * d;
};
int main() {
HasPtrMem a;
HasPtrMem b(a);
cout << *a.d << endl; // 0
cout << *b.d << endl; // 0
} // 析構:運行時錯誤砾脑,多次在同一位置調用delete
- 淺拷貝(shollow copy)
在未聲明拷貝構造函數(shù)時,編譯器也會為類生成一個淺拷貝構造函數(shù)辆憔。
解決淺拷貝問題的方法是用戶自定義拷貝函數(shù),進行“深拷貝”(deep copy)报嵌。
#include <iostream>
using namespace std;
class HasPtrMem {
public:
HasPtrMem(): d(new int(0)) {}
HasPtrMem(const HasPtrMem & h):
d(new int(*h.d)) {} // 拷貝構造函數(shù),從堆中分配內(nèi)存熊榛,并用*h.d初始化
~HasPtrMem() { delete d; }
int * d;
};
int main() {
HasPtrMem a;
HasPtrMem b(a);
cout << *a.d << endl; // 0
cout << *b.d << endl; // 0
} // 正常析構析構
左值锚国,右值和右值引用
在C++11中所有的值必屬于左值(lvalue)、右值(rvalue)兩者之一玄坦,右值又可以細分為純右值(prvalue, Pure RValue)血筑、將亡值(xvalue, eXpiring Value)。
在C++11中可以取地址的煎楣、有名字的就是左值豺总,反之,不能取地址的择懂、沒有名字的就是右值(將亡值或純右值)喻喳。
a = b+c;
// a 左值, &a 合法
// b+c 是右值困曙, &(b+c) 非法
純右值
- 非引用返回的函數(shù)返回的臨時變量值
- 1+3 產(chǎn)生的臨時變量
- 2, 'c', true
- 類型轉換函數(shù)的返回值
- lambda 表達式
將亡值
將亡值則是 C++11 新增的跟右值引用相關的表達式表伦,這樣表達式通常是將要被移動對象.
- 返回右值引用 T&& 的函數(shù)返回值
- std::move 的返回值
- 轉換為 T&& 的類型轉換函數(shù)的返回值
右值引用就是對一個右值進行引用的類型.
通常情況下,右值不具有名字,我們只能通過引用的方式找到它.
T&& a = RetureRvalue();
/*
* a 是右值引用
* RetureRvalue 返回一個臨時變量
* a 這個右值引用 引用 RetureRvalue 返回的臨時變量
*/
右值引用和左值引用都是屬于引用類型。無論是聲明一個左值引用還是右值引用慷丽,都必須立即進行初始化蹦哼。而其原因可以理解為是引用類型本身自己并不擁有所綁定對象的內(nèi)存,只是該對象的一個別名要糊。左值引用是具名變量值的別名纲熏,而右值引用則是不具名(匿名)變量的別名。
通常情況下,右值引用不能綁定到任何左值上.
int c;
int &&d = c; // error, 左值引用不能綁定到左值上
C++98 左值引用是否可以綁定到右值上?
T& e = RetureRvalue(); // error
const T& f = RetureRvalue(); // ok
在 C++98 中,常量左值引用就是個"萬能"的引用類型.可以接受非常量左值锄俄、常量左值局劲、右值對其進行初始化.而且使用右值對其初始化的時候,常量左值引用還可以向右值引用一樣將右值的生命周期延長.不過相比于右值引用所引用的右值,常量左值引用所引用的右值在它的"余生"中只能是只讀的.相對的,非常量左值引用只能接受非常量左值對其初始化.
在 C++98 中使用 常量左值引用 來減少臨時對象的開銷:
#include <iostream>
using namespace std;
struct Copyable {
Copyable() {}
Copyable(const Copyable &o) {
cout << "Copied" << endl;
}
};
Copyable ReturnRvalue() { return Copyable(); }
void AcceptVal(Copyable) {}
void AcceptRef(const Copyable & cp) {}//Copyable c = std::move(cp);}
void AcceptRRef(int && i) {i+=3; cout << (char)i << endl;}
int main() {
cout << "Pass by value: " << endl;
AcceptVal(ReturnRvalue()); // 臨時值被拷貝傳入
cout << "Pass by reference: " << endl;
AcceptRef(ReturnRvalue()); // 臨時值被作為引用傳遞
AcceptRRef('c'); // 臨時值被作為引用傳遞
}
常量右值引用
const T&& crvalueref = RetureRvalue();
右值引用就是為了移動語義,而移動語義需要右值是可以被修改的奶赠,那么常量右值引用在移動語義中就沒有用處容握;二來如果要引用右值且讓右值不可以更改,常量左值引用就夠了车柠。
引用類型 | 非常量左值 | 常量左值 | 非常量右值 | 常量右值 | 注記 |
---|---|---|---|---|---|
非常量左值引用 | Y | N | N | N | 無 |
常量左值引用 | Y | Y | Y | Y | 全部類型,可用于拷貝語義 |
非常量右值引用 | N | N | Y | N | 用于移動語義剔氏、完美轉發(fā) |
常量右值引用 | N | N | Y | Y | 暫無用途 |
#include <type_traits>
std::cout << is_lvalue_reference<string &&>::value;
std::cout << is_rvalue_reference<string &&>::value;
移動語義
#include <iostream>
using namespace std;
class A {
public:
int x;
A(int x) : x(x)
{
cout << "Constructor" << endl;
}
A(A& a) : x(a.x)
{
cout << "Copy Constructor" << endl;
}
A& operator=(A& a)
{
x = a.x;
cout << "Copy Assignment operator" << endl;
return *this;
}
A(A&& a) : x(a.x)
{
cout << "Move Constructor" << endl;
}
A& operator=(A&& a)
{
x = a.x;
cout << "Move Assignment operator" << endl;
return *this;
}
};
A GetA()
{
return A(1);
}
A&& MoveA()
{
return A(1);
}
int main()
{
cout << "-------------------------1-------------------------" << endl;
A a(1);
cout << "-------------------------2-------------------------" << endl;
A b = a;
cout << "-------------------------3-------------------------" << endl;
A c(a);
cout << "-------------------------4-------------------------" << endl;
b = a;
cout << "-------------------------5-------------------------" << endl;
A d = A(1);
cout << "-------------------------6-------------------------" << endl;
A e = std::move(a);
cout << "-------------------------7-------------------------" << endl;
A f = GetA();
cout << "-------------------------8-------------------------" << endl;
A&& g = MoveA();
cout << "-------------------------9-------------------------" << endl;
d = A(1);
}
/*
-------------------------1-------------------------
Constructor
-------------------------2-------------------------
Copy Constructor
-------------------------3-------------------------
Copy Constructor
-------------------------4-------------------------
Copy Assignment operator
-------------------------5-------------------------
Constructor
-------------------------6-------------------------
Move Constructor
-------------------------7-------------------------
Constructor
-------------------------8-------------------------
Constructor
-------------------------9-------------------------
Constructor
Move Assignment operator
*/
#include <iostream>
using namespace std;
class HasPtrMem {
public:
HasPtrMem(): d(new int(0)) {
cout << "Construct: " << ++n_cstr << endl;
}
HasPtrMem(const HasPtrMem & h): d(new int(*h.d)) {
cout << "Copy construct: " << ++n_cptr << endl;
}
~HasPtrMem() {
delete d;
cout << "Destruct: " << ++n_dstr << endl;
}
int * d;
static int n_cstr;
static int n_dstr;
static int n_cptr;
};
int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
HasPtrMem GetTemp() { return HasPtrMem(); }
int main() {
HasPtrMem a = GetTemp();
}
/*
* 程序輸出:
* Construct: 1
* Copy construct: 1
* Destruct: 1
* Copy construct: 2
* Destruct: 2
* Destruct: 3
*/
/*
* 在新的版本的編譯器程序輸出:
* Construct: 1
* Destruct: 1
*/
本示例想說明一個問題塑猖,拷貝構造的調用,尤其是深拷貝構造的調用谈跛,會進行內(nèi)存的memcpy羊苟,消耗大量資源。
C++11 的移動語義(move semantics):
不會進行拷貝構造感憾,只是將臨時變量“偷來”蜡励。
#include <iostream>
using namespace std;
class HasPtrMem {
public:
HasPtrMem(): d(new int(3)) {
cout << "Construct: " << ++n_cstr << endl;
}
HasPtrMem(const HasPtrMem& h) : d(new int(*h.d)) {
cout << "Copy construct: " << ++n_cptr << endl;
}
HasPtrMem(HasPtrMem && h): d(h.d) { // 移動構造函數(shù)
h.d = nullptr; // 將臨時值的指針成員置空
cout << "Move construct: " << ++n_mvtr << endl;
}
~HasPtrMem() {
delete d;
cout << "Destruct: " << ++n_dstr << endl;
}
int * d;
static int n_cstr;
static int n_dstr;
static int n_cptr;
static int n_mvtr;
};
int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_mvtr = 0;
const HasPtrMem GetTemp() {
HasPtrMem h;
cout << "Resource from " << __func__ << ": " << hex << h.d << endl;
return h;
}
int main() {
const HasPtrMem && a = GetTemp();
cout << "Resource from " << __func__ << ": " << hex << a.d << endl;
// hex << a.d ===> hex(a.d)
}
/*
Construct: 1
Resource from GetTemp: 0x907ab0
Resource from main: 0x907ab0
Destruct: 1
*/
// 移動構造并沒有被調用,該示例無法說明任何問題.
std::move 強制轉換為右值
std::move 將一個左值強制轉換為右值引用阻桅,繼而我們通過右值使用該值凉倚,以用于移動語義。
// 等價于:
static_cast<T&&>(lvalue);
被強制轉化的左值嫂沉,其生命周期并沒有隨著左右值的變化而改變稽寒。
#include <iostream>
class Moveable {
public:
Moveable(): i (new int(3)) {
std::cout << "Moveable" << std::endl;
}
~Moveable() { delete i; }
Moveable(const Moveable & m) : i(new int(*m.i)) {}
Moveable(Moveable && m) : i (m.i) {
m.i = nullptr;
}
int *i;
};
int main()
{
Moveable a;
Moveable c(std::move(a)); // a 為左值,強制轉換為右值
std::cout << *(a.i) << std::endl; // 在 std::move(a) 時趟章, a.i 就被設置為了 nullptr杏糙, 故這里運行時錯誤
return 0;
}