13.6 對象移動
這一節本質上是對性能進行優化,(對設計資源管理的類的性能優化),就算不知道這一節的的內容,一樣的可以編碼,但是如果設計的類對性能要求較高,就需要這一節的內容了。
在使用vector裝自定義的類型的類型時,如果容量不足會開闢空間,將原來的元素拷貝到新的空間的,原來的元素會被銷燬,我們知道原來的元素拷貝到新空間之後,不會再使用,如果對象非常的大,那麼拷貝是會消耗比較大的性能的,既然原來的對象拷貝之後就不再使用,那麼有沒有什麼方法,能夠讓原本對象的內容直接拷貝到新的內存空間中呢,C++提供了移動操作,用來將原來對象“移動”到新的內存空間中。使用移動的方式可以避免拷貝。
目前看來,“移動”操作和swap()類型可以大幅度提升性能的前提是類中有標準庫容器,或者有指針指向的動態分配的對象。
13.6.1 右值引用
因爲要支持移動操作,C++定義了右值引用。
使用&&來獲取右值引用,有了右值引用,我們就可以其綁定的資源“移動”到另一個對象中。
右值引用用來綁定字面值常量,返回右值的表達式以及要求轉換類型的表達式 , (要求轉換類型的意思應該是表達式中既有左值也有右值) ,
左值具有持久的狀態,知道離開作用域才被銷燬,而右值是字面值常量要麼是表達式中的臨時對象,所引用的對象即將被銷燬且沒有其他的用戶。
所有的變量都是左值,因爲右值引用的變量也是左值,而左值不能直接使用右值綁定。
需要使用std::move(obj)將左值轉化爲右值類型,但是這樣意味着obj的資源將被右值引用的變量所接管,後續除了對obj進行賦值和銷燬,我們不要再使用它。
練習
13.45
一句話:
左值引用可以綁定左值,const 左值引用和右值引用可以綁定右值。
左值引用可以綁定返回值類型爲引用的函數、賦值、下標、解引用、前置遞減遞增運算符等返回左值的表達式的結果上。
const類型的左值引用和右值引用可以綁定在返回非引用類型的函數、算術、關係、位、後置遞增遞減等返回右值的表達式的結果上。
13.46
int &&r1 = f();
int &r2 = vi[0];
int & r3 = r1;
int &&r4 = vi[0] * f();
13.47
這裏完善了之前的MyString類,添加了拷貝構造函數、拷貝內賦值運算符、free()等函數
.h文件
#pragma once
#include <memory>
class MyString {
friend void print(std::ostream& s, const MyString& str);
public:
MyString();
MyString(const char*);
MyString(const MyString&);
MyString& operator=(const MyString&);
~MyString();
size_t size()const;
private:
size_t get_char_arr_len(const char *);
void free();
static std::allocator<char> alloc;
char* begin;
char* end;
char* last;
};
.cpp文件
#include "pch.h"
#include "MyString.h"
#include <algorithm>
#include <iostream>
using std::cout;
using std::endl;
std::allocator<char> MyString::alloc;
MyString::MyString()
{
begin = alloc.allocate(1);
alloc.construct(begin, '\0');
end = begin;
last = end + 1;
}
MyString::MyString(const char * c)
{
size_t len = get_char_arr_len(c) + 1;
begin = alloc.allocate(len);
end = begin + len - 1;
last = end + 1;
size_t index = 0;
for (auto iter = begin; iter != end; ++iter)
{
alloc.construct(iter, c[index]);
++index;
}
*end = '\0';
}
MyString::MyString(const MyString & s)
{
cout << "調用了拷貝構造函數" << endl;
auto temp_begin = alloc.allocate(s.size() + 1);
auto temp_iter = temp_begin;
for (auto iter = s.begin; iter != s.end; ++iter)
{
alloc.construct(temp_iter++, *iter);
}
begin = temp_begin;
end = temp_iter;
*end = '\0';
last = end + 1;
}
MyString & MyString::operator=(const MyString &s)
{
cout << "調用了賦值運算符" << endl;
auto temp_begin = alloc.allocate(s.size()+1);
auto temp_iter = temp_begin;
for (auto iter=s.begin;iter!=s.end;++iter)
{
alloc.construct(temp_iter++, *iter);
}
free();
// TODO: 在此處插入 return 語句
begin = temp_begin;
end = temp_iter;
*end = '\0';
last = end + 1;
return *this;
}
MyString::~MyString()
{
free();
}
size_t MyString::size()const
{
return end - begin;
}
size_t MyString::get_char_arr_len(const char * c)
{
size_t len = 0;
while (*c != '\0')
{
++len;
++c;
}
return len;
}
void MyString:: free()
{
std::for_each(begin, end + 1, [](const char& item) {
alloc.destroy(&item);
});
alloc.deallocate(begin, last - begin);
}
void print(std::ostream& s, const MyString& str)
{
std::for_each(str.begin, str.end, [&s](const char& item) {
s << item;
});
}
13.48
每次重新分配內存空間,則會將原來的元素都拷貝一次。
vector<MyString> vec = {"233","333"};//兩次拷貝
cout << "------------" << endl;
vec.push_back("233");//一次拷貝+兩個拷貝函數
cout << "------------" << endl;
vec.push_back("233");//一次拷貝+三次拷貝
cout << "------------" << endl;
output
C風格構造函數
C風格構造函數
調用了拷貝構造函數
調用了拷貝構造函數
------------
C風格構造函數
調用了拷貝構造函數
調用了拷貝構造函數
調用了拷貝構造函數
------------
C風格構造函數
調用了拷貝構造函數
調用了拷貝構造函數
調用了拷貝構造函數
調用了拷貝構造函數
------------
13.6.2 移動構造函數,移動賦值運算符
有了右值引用,提升類的性能,就需要用到移動構造函數和移動賦值運算符。
移動構造函數形參和移動賦值運算符的基本要求都和拷貝構造拷貝賦值運算符一樣。
只是他們的類型爲&&。而且通常不加const,因爲我們要修改右值引用的對象的數據成員。
class A
{
public:
A():value(nullptr){};
~A() {
delete value;
};
//拷貝構造函數
A(const A&) {};
//移動構造
A(A&& a) noexcept {
//將右值引用的資源轉讓給本對象
value = a.value;
//右值引用的對象的資源放置爲安全的狀態
a.value = nullptr;
};
//拷貝賦值
A& operator=(const A&) {};
//移動賦值
A& operator=(A&&a) noexcept {
if (this!=&a)
{
delete value;
value = a.value;
a.value = nullptr;
}
return *this;
};
private:
string* value;
};
如果在拷貝和賦值時傳入的對象時右值類型,則會調用移動構造和移動賦值。在移動構造和移動賦值中,需要將右值引用的對象的資源賦值給本對象,然後將右值引用的對象的資源置爲一個安全狀態。
被移動後的對象叫做移後源對象,在被移動之後,我們必須確保移後源對象可以被正確的析構,且我們不再訪問它的數據成員,因爲不能保證它的數據成員此時還有效。
通常情況下我們會爲移動構造和移動賦值加上noexcept表示,這兩個函數絕對不會發生異常。noexcept表示我們自己承諾這個函數不會拋出異常,它加在參數列表和初始化列表的冒號:之間,且聲明和定義都要有。只有加上了noexcept,在容器需要重新分配空間拷貝原來的元素時,容器纔會執行移動構造,否則執行拷貝構造,如果沒有使用容器來裝自定義的類,那麼noexcept不寫也沒事。
合成的移動操作
和拷貝構造、拷貝賦值一樣,移動操作也有合成的版本,但是和拷貝構造、拷貝賦值的條件不一樣。
只要我們沒有定義拷貝構造、拷貝賦值、析構函數,編譯器就會爲我們的類合成一個。
只要拷貝構造、拷貝賦值、析構我們自定義了一個,那麼就不會合成移動操作。 因爲移動操作算是優化,就算沒有移動操作,拷貝構造、拷貝賦值、析構程序一樣的可以完成所有功能。
只有當我們沒有定義任何拷貝控制成員和類的所有非static數據成員都可以移動時,移動操作纔會被合成
另一方面,如果我們定義了移動構造,則編譯器不會爲我們合成拷貝構造,定義了移動賦值則編譯器不會我們生成拷貝賦值。
關於什麼時候變爲已刪除的函數。所謂的以刪除就是編譯器不會爲我們合成這些函數。
如果我們同時定義了拷貝構造、賦值和移動構造、賦值,則如果傳入的對象是右值類型的,則調用移動構造、賦值,否則調用拷貝構造、賦值。
如果沒有定義移動構造、賦值傳入的是右值,則拷貝構造、賦值會處理。
移動迭代器
之前的迭代器介紹時,除了流迭代器,插入迭代器、反向還有移動迭代器。
移動迭代器顧名思義就是爲了用來迭代器的,移動迭代器改變了迭代器的解引用操作,其解引用返回的是一個右值引用。
我們使用make_move_iterator(iter)來得到一個迭代器的移動迭代器類型。
如果一個對象爲右值引用類型。那麼下面的代碼將調用移動賦值運算符(如果有的話)
*iter = *make_move_iterator(iter1);
移動迭代器一般用在算法中,但是有些算法可能會改變傳入的迭代器的值,所以我們需要確保使用移動迭代器時,需要確保被移動的對象不再被使用。
13.6.3 右值引用和成員函數
我們定義形參爲右值引用版本的成員函數,它和其他同名成員函數構成重載。
void A::func(A&) {
}
void A::func(A&&) {
}
左值和右值引用成員函數
下面的代碼是成立的,因爲我們=被重載過了,這裏爲右值賦值其實是調用一個成員函數,而成員函數調用時不用管是否是左值還是右值。
string str1 = "123";
string str2 = "233";
(str1 + str2) = "123";
如果我們區分左值和右值成員函數的操作的話,那麼可以通過在函數的形參列表後面加&或者&&來限定此成員函數由左值還是右值對象來調用。
//左值類型的對象調用這個函數
void A::func1(A) &
{
}
//右值類型的對象調用這個函數
void A::func2(A) &&
{
}
引用限定符參與函數的重載,需要注意引用限定符必須在const限定符的右側
void A::func1(A) const &{
}
如果我們對兩個或者兩個以上的同名,同參數成員函數中使用引用限定符,那麼要麼全部使用引用限定符,那麼一個都不使用。
練習
13.49
StrVec
StrVec::StrVec(StrVec &&s) noexcept
{
cout<<"調用移動構造函數"<<endl;
elements = s.elements;
first_free = s.first_free;
cap = s.cap;
//確保其可以被析構
s.elements = s.first_free = s.cap = nullptr;
}
StrVec & StrVec::operator=(StrVec &&s)noexcept
{
cout<<"-----------"<<endl;
cout<<"調用移動賦值運算符"<<endl;
if (this!=&s) {
free();
elements = s.elements;
first_free = s.first_free;
cap = s.cap;
//確保其可以被析構
s.elements = s.first_free = s.cap = nullptr;
}
// TODO: 在此處插入 return 語句
return *this;
}
MyString
StrVec::StrVec(StrVec &&s) noexcept
{
cout<<"調用移動構造函數"<<endl;
elements = s.elements;
first_free = s.first_free;
cap = s.cap;
//確保其可以被析構
s.elements = s.first_free = s.cap = nullptr;
}
StrVec & StrVec::operator=(StrVec &&s)noexcept
{
cout<<"-----------"<<endl;
cout<<"調用移動賦值運算符"<<endl;
if (this!=&s) {
free();
elements = s.elements;
first_free = s.first_free;
cap = s.cap;
//確保其可以被析構
s.elements = s.first_free = s.cap = nullptr;
}
// TODO: 在此處插入 return 語句
return *this;
}
Message
在這裏我直接理所當然的將contents=m.contents,而沒有考慮使用move,這是錯誤的,之前的兩個類之所以沒有寫move是因爲他們本來就是指針類型。而Message的數據成員都不是指針類型,而是類類型,所以想要移動他們的元素,就需要使用move來調用他們移動構造函數或者移動賦值運算符
Message::Message(Message &&m)
{
contents = std::move(m.contents);
folders = std::move(m.folders);
for (const auto &f:folders)
{
f->remMsg(&m);
f->addMsg(this);
}
m.folders.clear();
}
Message & Message::operator=(Message &&m)
{
if (this!=&m)
{
contents = std::move(m.contents);
remove_from_Folders();
folders = std::move(m.folders);
for (const auto &f : folders)
{
f->remMsg(&m);
f->addMsg(this);
}
m.folders.clear();
}
// TODO: 在此處插入 return 語句
return *this;
}
13.50
對於13.48中的輸出可以看到,當容器的空間不足開闢新空間拷貝元素時調用的是移動構造函數而不是拷貝構造函數。
同時這裏也 複習到了,隱式轉換的時候是將傳入的值先變爲一個對象,然後再做拷貝或者移動
vector<MyString> vec = { "123","321" };//兩次拷貝
//vector<MyString> vec = {MyString("123"),MyString("321")};//兩次拷貝
cout << "------------" << endl;
vec.push_back("233");//
cout << "------------" << endl;
vec.push_back("233");//
cout << "------------" << endl;
--output-
C風格構造函數
C風格構造函數
調用了拷貝構造函數
調用了拷貝構造函數
------------
C風格構造函數
調用了移動構造函數
調用了移動構造函數
調用了移動構造函數
------------
C風格構造函數
調用了移動構造函數
調用了移動構造函數
調用了移動構造函數
調用了移動構造函數
------------
13.51
因爲unique_ptr定義了移動構造函數和移動賦值運算符,所以調用clone()返回的時候,在調用處的匿名對象是通過移動構造函數來獲得返回的unique_ptr的所有權的。調用結束後,只有匿名unique_ptr對象指向它維護的對象,而函數內部的unique_ptr已經被銷燬了。
13.52
因爲hp2是一個對象,所有對象都是左值。
所以hp=hp2.將調用拷貝賦值運算符。通過將hp2傳入到HasPtr的拷貝運算符函數中,因爲rhs是非引用類型,所以將調用拷貝初始化來初始化rhs。
所以rhs和hp2的內容將是獨立的。
通過swap交換內部兩個對象的數據,返回*this。完成hp的賦值,此時rhs的生命週期結束,系統銷燬並回收內存。
hp=std::move(hp2),雖然hp2是左值,但是可以調用std::move()將一個右值引用綁定到hp2上,此時表達式右側是右值引用,那麼使用拷貝構造函數和移動構造函數都可以,但是拷貝構造函數首先要將右值引用轉換爲const類型的左值引用,但是移動賦值運算符精確匹配,所以將調用移動賦值運算符,來對hp賦值
賦值之後認爲hp2原來的數據都有hp來操縱,所以後續的代碼中不對hp2進行任何操作。
13.53
HasPtr& operator=(const HasPtr& p) {
cout << "拷貝賦值" << endl;
if (this!=&p) {
auto temp = new string(*p.ps);
delete ps;
ps = temp;
i = p.i;
}
return *this;
};
HasPtr& operator=( HasPtr p) {
cout << "拷貝賦值" << endl;
if (this!=&p) {
swap(*this, p);
}
return *this;
};
HasPtr& operator=( HasPtr&& p) {
cout << "移動賦值" << endl;
if (this!=&p)
{
delete ps;
ps = p.ps;
i = p.i;
p.ps = nullptr;
}
return *this;
}
對比沒用swap的版本,可以看到拷貝賦值中需要動態分配內存。
而使用swap版本的拷貝賦值,則需要調用拷貝構造函數創建一個形參。
使用移動賦值既不需要調用拷貝構造也不需要動態分配內存。
13.54
如果使用這個版本的拷貝賦值運算符,則二者會發生衝突,報錯爲operator=不明確。
使用引用版本的拷貝賦值運算符不會發生衝突。
HasPtr& operator=( HasPtr p) {
cout << "拷貝賦值" << endl;
if (this!=&p) {
swap(*this, p);
/*auto temp = new string(*p.ps);
delete ps;
ps = temp;
i = p.i;*/
}
return *this;
};
13.6.3右值引用與成員函數
練習
13.55
void StrVec::push_back(string && s)
{
chk_n_alloc();
//這樣做的前提是string類型專門對右值做了優化
//否則和普通的push_back沒什麼兩樣
alloc.construct(first_free++, std::move(s));
}
13.56
ret是左值,所以調用的是左值版本的sorted()函數,所以會形成死循環。
13.57
因爲直接在return語句上創建對象,所以Foo(*this)實際上是一個右值,所以將調用右值版本的sorted();不會死循環。
13.58
class Foo {
public:
Foo sorted() && ;
Foo sorted() const&;
Foo(initializer_list<int> init_list) :data(init_list) {};
private:
vector<int>data;
};
Foo Foo::sorted() &&
{
cout<<"調用對象爲右值"<<endl;
sort(data.begin(), data.end());
return *this;
}
Foo Foo::sorted() const &
{
cout<<"調用對象爲左值"<<endl;
/*Foo ret(*this);
sort(ret.data.begin(), ret.data.end());
return ret;*/
//Foo ret(*this);
return Foo(*this).sorted();
}
Foo retVal() {
Foo foo({123,14,3,14,123,4,213,41,123,12321});
return foo;
}
Foo f11({123,4,3124,2134,2314,234112354,657,756543,345,765,3427,6758});
Foo& retFoo() {
return f11;
}
---
//retVal().sorted();
retFoo().sorted();