對拷貝控制成員使用= default
我們可以通過將拷貝控制成員定義為= default
践叠,顯示地要求編譯器生成它們的合成版本:
Code:
class Sales_data {
public:
// copy control; use defaults
Sales_data() = default;
Sales_data(const Sales_data&) = default;
Sales_data& operator=(const Sales_data &);
~Sales_data() = default;
// other members as before
};
Sales_data& Sales_data::operator=(const Sales_data&) = default;
當(dāng)我們在類體內(nèi)的成員聲明中指定= default
時(shí)禁灼,編譯器生成的合成函數(shù)是隱式內(nèi)聯(lián)的(就像在類體中定義的任何其他成員函數(shù)一樣)轿曙。如果我們不希望合成函數(shù)是內(nèi)聯(lián)函數(shù)导帝,我們可以在該函數(shù)的定義上指定= default
,就像在重載=
運(yùn)算符的定義中那樣斋荞。
注:我們只能對具有合成版本的成員函數(shù)使用= default
(即默認(rèn)構(gòu)造函數(shù)或拷貝控制成員)平酿。
使用= delete來阻止拷貝類對象
在新標(biāo)準(zhǔn)下悦陋,我們可以通過將拷貝構(gòu)造函數(shù)和賦值運(yùn)算符定義為已刪除函數(shù)來阻止復(fù)制叨恨。已刪除的函數(shù)是已聲明的函數(shù),但不能以任何其他方式使用秉颗。我們使用= delete
跟隨想要?jiǎng)h除的函數(shù)的參數(shù)列表來將函數(shù)定義為已刪除:
Code:
struct NoCopy {
NoCopy() = default; // use the synthesized default constructor
NoCopy(const NoCopy&) = delete; // no copy
NoCopy &operator=(const NoCopy&) = delete; // no assignment
~NoCopy() = default; // use the synthesized destructor
// other members
}
= delete
關(guān)鍵字即告訴編譯器又告訴代碼閱讀者蚕甥,我們故意沒有定義這些成員栋荸。
與= default
不同,= delete
必須出現(xiàn)在已刪除函數(shù)的第一個(gè)聲明中帅霜。從邏輯上講呼伸,這種差異源自這些聲明的含義。默認(rèn)成員只影響編譯器生成的代碼搂根;因此在編譯器生成代碼之前不需要= default
铃辖。另一方面娇斩,編譯器需要知道一個(gè)函數(shù)被刪除,以禁止試圖使用它的操作五督。
與= default
不同充包,我們可以在任何函數(shù)上指定= delete
(我們可以在默認(rèn)構(gòu)造函數(shù)或編譯器可以合成的拷貝控件成員上使用= default
)遥椿。雖然刪除函數(shù)的主要用途是抑制拷貝控制成員冠场,但是當(dāng)我們想要引導(dǎo)函數(shù)匹配過程時(shí),刪除函數(shù)有時(shí)也很有用钢悲。
移動(dòng)而不是復(fù)制類對象
我們可以使用新標(biāo)準(zhǔn)庫引入的兩個(gè)工具來避免復(fù)制字符串莺琳。首先载慈,包括字符串在內(nèi)的幾個(gè)庫類定義了所謂的“移動(dòng)構(gòu)造函數(shù)”办铡。字符串移動(dòng)構(gòu)造函數(shù)如何工作的詳細(xì)信息與其他任何有關(guān)實(shí)現(xiàn)的詳細(xì)信息都沒有公開琳要。但是稚补,我們知道移動(dòng)構(gòu)造函數(shù)通常通過將資源從給定對象“移動(dòng)”到正在構(gòu)造的對象來操作嫂伞。我們也知道庫保證“移動(dòng)”字符串保持有效帖努,可破壞的狀態(tài)粪般。對于字符串亩歹,我們可以想象每個(gè)字符串都有一個(gè)指向char
數(shù)組的指針。據(jù)推測亭姥,字符串移動(dòng)構(gòu)造函數(shù)復(fù)制指針而不是為字符本身分配空間和復(fù)制字符达罗。
我們將使用的第二個(gè)工具是名為move
的庫函數(shù)静秆,它在頭文件<utility>
中定義(關(guān)于utility
更多的信息可參考header <utility>)抚笔。目前殊橙,關(guān)于move
有兩點(diǎn)需要了解。首先螃概,當(dāng)reallocate
在新內(nèi)存中構(gòu)造字符串時(shí)鸽疾,它必須調(diào)用move
來表示它想要使用字符串移動(dòng)構(gòu)造函數(shù)制肮。如果省略了調(diào)用move
來移動(dòng)字符串,則將使用拷貝構(gòu)造函數(shù)综液。其次谬莹,我們通常不提供move
的使用說明附帽。當(dāng)我們使用move
時(shí),我們調(diào)用std::move
整胃,而不是move
喳钟。關(guān)于move
的更多的信息可參考std::move奔则。
右值引用
為了支持move
操作,新標(biāo)準(zhǔn)引入了一種新的引用酬蹋,即右值應(yīng)用除嘹。右值引用是必須綁定到右值的引用岸蜗。通過使用&&
而不是&
獲得右值引用璃岳。正如我們將看到的铃慷,右值引用具有重要的屬性,它們可能只綁定到即將被銷毀的對象上洲鸠。因此,我們可以自由地將資源從一個(gè)右值引用“移動(dòng)”到另一個(gè)對象。
左值和右值是表達(dá)式的屬性扒腕。有些表達(dá)式產(chǎn)生或需要左值绢淀;另一些表達(dá)式產(chǎn)生或需要右值。一般來說瘾腰,左值表達(dá)式指的是對象的標(biāo)識皆的,而右值表達(dá)式指的是對象的值。
與任何引用一樣蹋盆,右值引用只是對象的另一個(gè)名稱费薄。我們知道栖雾,當(dāng)我們需要將它們與右值引用區(qū)分開時(shí)楞抡,我們將其稱為左值引用,我們不能將常規(guī)引用綁定到需要轉(zhuǎn)換的表達(dá)式岩灭,文本或返回右值的表達(dá)式拌倍。右值引用具有相反的綁定屬性:我們可以將右值引用綁定到這些類型的表達(dá)式赂鲤,但是我們不能直接將右值引用綁定到左值:
Code:
int i = 42;
int &r = i; // ok: r refers to i
int &&rr = i; // error: cannot bind an rvalue reference to an lvalue
int &r2 = i * 42; // error: i * 42 is an rvalue
const int &r3 = i * 42; // ok: we can bind a reference to const to an rvalue
int &&rr2 = i * 42; // ok: bind rr2 to the result of the multiplication
返回左值引用的函數(shù)以及賦值噪径,下標(biāo),解引用和前綴增量/減量運(yùn)算符都是返回左值的表達(dá)式的示例数初。我們可以將左值引用綁定到任何這些表達(dá)式的結(jié)果找爱。
返回非引用類型的函數(shù),以及算術(shù)泡孩,關(guān)系车摄,按位和后綴增量/減量運(yùn)算符,都產(chǎn)生右值仑鸥。我們不能將左值引用綁定到這些表達(dá)式吮播,但我們可以綁定對const
的左值引用或?qū)@些表達(dá)式的右值引用。
庫函數(shù)move
雖然我們不能直接將右值引用綁定到左值眼俊,但我們可以顯式地將左值轉(zhuǎn)換為其對應(yīng)的右值引用類型意狠。我們還可以通過調(diào)用名為move
的新標(biāo)準(zhǔn)庫函數(shù)來獲取綁定到左值的右值引用,該函數(shù)在utility
頭文件中定義疮胖。move
函數(shù)的定義如下:
Code:
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
我們可以以如下方式使用move
函數(shù);
Code:
int &&rr3 = std::move(rr1); // ok
調(diào)用move
告訴編譯器我們有一個(gè)左值环戈,我們要將它視為右值。必須認(rèn)識到move
承諾澎灸,我們不打算再次使用rr1
院塞,除非對它賦值或銷毀它。在調(diào)用move
之后性昭,我們無法對被操作對象的值做出任何假設(shè)拦止。
注:我們可以銷毀一個(gè)被move
操作的對象,也可以為它分配一個(gè)新值糜颠,但是我們不能使用它的值汹族。
注:使用move
的代碼應(yīng)該使用std::move
艺玲,而不是move
。這樣做可以避免潛在的名稱沖突鞠抑。
移動(dòng)構(gòu)造和移動(dòng)賦值
我們可以在自己的類中定義移動(dòng)構(gòu)造和移動(dòng)賦值函數(shù)饭聚,這種函數(shù)與拷貝構(gòu)造函數(shù)類型,但它們從給定對象中“竊取資源”而不是復(fù)制資源搁拙。
與拷貝構(gòu)造函數(shù)一樣秒梳,移動(dòng)構(gòu)造函數(shù)具有一個(gè)初始參數(shù),該參數(shù)是對類類型的引用箕速。與拷貝構(gòu)造函數(shù)不同酪碘,移動(dòng)構(gòu)造函數(shù)中的引用參數(shù)是右值引用。與拷貝構(gòu)造函數(shù)一樣盐茎,任何其他參數(shù)都必須具有默認(rèn)參數(shù)兴垦。
除了移動(dòng)資源之外,移動(dòng)構(gòu)造函數(shù)還必須確保移動(dòng)的對象處于一種合法狀態(tài)字柠,以便無影響地銷毀該對象探越。特別是,一旦移動(dòng)其資源窑业,原始對象就不能再指向那些被移動(dòng)的資源钦幔,而新創(chuàng)建的對象負(fù)責(zé)這些被移動(dòng)的資源。
作為一個(gè)例子常柄,我們將定義StrVec
移動(dòng)構(gòu)造函數(shù)來移動(dòng)而不是將元素從一個(gè)StrVec
復(fù)制到另一個(gè):
Code:
/***********************
class StrVec{
private:
std::string *elements; // pointer to the first element in the array
std::string *first_free; // pointer to the first free element in the array
std::string *cap; // pointer to one past the end of the array
};
***********************/
StrVec::StrVec(StrVec &&s) noexcept // move won't throw any exceptions
// member initializers take over the resources in s
: elements(s.elements), first_free(s.first_free), cap(s.cap)
{
// leave s in a state in which it is safe to run the destructor
s.elements = s.first_free = s.cap = nullptr;
}
與復(fù)制構(gòu)造函數(shù)不同鲤氢,移動(dòng)構(gòu)造函數(shù)不分配任何新內(nèi)存;它接管給定StrVec
中的內(nèi)存西潘。從其參數(shù)接管內(nèi)存后卷玉,構(gòu)造函數(shù)體將給定對象中的指針設(shè)置為nullptr
。 移動(dòng)對象后喷市,該對象繼續(xù)存在相种。最終,移動(dòng)的對象將被銷毀东抹,這意味該對象將運(yùn)行析構(gòu)函數(shù)蚂子。StrVec
析構(gòu)函數(shù)在first_free
上調(diào)用deallocate
。如果我們忽略了改變s.first_free
缭黔,那么銷毀給定對象將刪除我們剛剛移動(dòng)的資源食茎。
移動(dòng)構(gòu)造函數(shù)通常應(yīng)該是noexcept
因?yàn)橐苿?dòng)操作通過“竊取”資源來執(zhí)行,所以它通常不會自己分配任何資源馏谨。 因此别渔,移動(dòng)操作通常不會引發(fā)任何異常。當(dāng)我們編寫一個(gè)不能拋出異常的移動(dòng)操作時(shí),我們應(yīng)該告知庫這個(gè)事實(shí)哎媚。正如我們所看到的喇伯,除非庫知道移動(dòng)構(gòu)造函數(shù)不會拋出異常,否則它將做額外的工作拨与,以滿足移動(dòng)類類型的對象可能拋出異常的可能性稻据。
通知庫的一種方法是在構(gòu)造函數(shù)上指定noexcept
。noexcept
是我們承諾函數(shù)不會拋出任何異常的一種方式买喧。我們在函數(shù)的參數(shù)列表之后指定noexcept
捻悯。在構(gòu)造函數(shù)中,noexcept
出現(xiàn)在參數(shù)列表和以:
開始的成員初始化列表之間:
Code:
class StrVec {
public:
StrVec(StrVec&&) noexcept; // move constructor
// other members as before
};
StrVec::StrVec(StrVec &&s) noexcept : // member initializers
{ /* constructor body */ }
如果函數(shù)的定義出現(xiàn)在類之外淤毛,我們必須在類頭中的聲明和定義上都指定noexcept
今缚。
理解為什么需要noexcept
可以幫助加深我們對庫如何與我們編寫的類型的對象交互的理解。我們需要指出一個(gè)移動(dòng)操作不會拋出異常低淡,因?yàn)橛袃蓚€(gè)相互關(guān)聯(lián)的事實(shí):第一姓言,盡管移動(dòng)操作通常不拋出異常,但允許它們這樣做蔗蹋。第二何荚,庫容器提供了在發(fā)生異常時(shí)所做操作的保證。例如纸颜,vector
保證如果在我們調(diào)用push_back
時(shí)發(fā)生異常兽泣,那么向量本身將保持不變绎橘。
現(xiàn)在讓我們考慮一下push_back
內(nèi)部會發(fā)生什么胁孙。與相應(yīng)的StrVec
操作一樣,vector
上的push_back
可能需要重新分配向量称鳞。當(dāng)重新分配向量時(shí)涮较,它會將元素從其舊空間移動(dòng)到新內(nèi)存。
正如我們之前所見冈止,移動(dòng)一個(gè)對象通常會更改被移動(dòng)的對象的值狂票。如果重新分配使用移動(dòng)構(gòu)造函數(shù),并且該構(gòu)造函數(shù)在移動(dòng)一些但不是所有元素后拋出異常熙暴,則會出現(xiàn)問題闺属。舊空間中的已經(jīng)移動(dòng)的元素將被更改,新空間中未構(gòu)造的元素將不存在周霉。在這種情況下掂器,vector
將無法滿足其保持向量不變的要求。
另一方面俱箱,如果vector
使用復(fù)制構(gòu)造函數(shù)并發(fā)生異常国瓮,則可以輕松滿足此要求。在這種情況下,當(dāng)元素在新內(nèi)存中構(gòu)造時(shí)乃摹,舊元素保持不變禁漓。如果發(fā)生異常,vector
可以釋放它已經(jīng)分配的但無法成功構(gòu)造的空間并返回孵睬。原始的vector
元素仍然存在播歼。
為了避免這個(gè)潛在的問題,vector
在重新分配期間必須使用拷貝構(gòu)造函數(shù)而不是移動(dòng)構(gòu)造函數(shù)掰读,除非它知道元素類型的移動(dòng)構(gòu)造函數(shù)不能拋出異常荚恶。如果我們希望移動(dòng)我們類型的對象而不是在vector
重新分配等情況下復(fù)制,我們必須明確告訴庫我們的移動(dòng)構(gòu)造函數(shù)是安全的磷支。我們通過標(biāo)記移動(dòng)構(gòu)造函數(shù)(和移動(dòng)賦值運(yùn)算符)noexcept
來實(shí)現(xiàn)谒撼。
移動(dòng)迭代器
我們在重新分配成員時(shí)可以使用for
循環(huán)來調(diào)用構(gòu)造函數(shù)以將元素從舊內(nèi)存復(fù)制到新內(nèi)存。作為編寫該循環(huán)的替代方法雾狈,如果我們可以調(diào)用uninitialized_copy
來構(gòu)造新分配的空間廓潜,那將會更容易。但是uninitialized_copy
只做復(fù)制工作善榛。沒有類似的庫函數(shù)可以將對象“移動(dòng)”到未構(gòu)造的內(nèi)存中辩蛋。
相反,新標(biāo)準(zhǔn)庫定義了一個(gè)移動(dòng)迭代器適配器移盆。移動(dòng)迭代器通過更改迭代器的解引用運(yùn)算符的行為來調(diào)整其給定的迭代器悼院。通常,迭代器解引用運(yùn)算符返回對該元素的左值引用咒循。與其他迭代器不同据途,移動(dòng)迭代器的取消引用運(yùn)算符產(chǎn)生右值引用。
我們通過調(diào)用庫make_move_iterator
函數(shù)將普通迭代器轉(zhuǎn)換為移動(dòng)迭代器叙甸。此函數(shù)接收迭代器并返回移動(dòng)迭代器颖医。
返回的移動(dòng)迭代器的所有操作都和原始迭代器一樣。因?yàn)檫@些迭代器支持常規(guī)迭代器操作裆蒸,所以我們可以將一對移動(dòng)迭代器傳遞給算法熔萧。特別是,我們可以將移動(dòng)迭代器傳遞給uninitialized_copy
:
Code:
void StrVec::reallocate()
{
// allocate space for twice as many elements as the current size
auto newcapacity = size() ? 2 * size() : 1;
auto first = alloc.allocate(newcapacity);
// move the elements
auto last = uninitialized_copy(make_move_iterator(begin()),
make_move_iterator(end()),
first);
free(); // free the old space
elements = first; // update the pointers
first_free = last;
cap = elements + newcapacity;
}
uninitialized_copy
調(diào)用輸入序列中每個(gè)元素的構(gòu)造函數(shù)僚祷,以將該元素“復(fù)制”到目標(biāo)中佛致。該算法使用迭代器解引用運(yùn)算符從輸入序列中獲取元素。因?yàn)槲覀儌鬟f了移動(dòng)迭代器辙谜,所以解引用運(yùn)算符產(chǎn)生一個(gè)右值引用俺榆,這意味著構(gòu)造將使用移動(dòng)構(gòu)造函數(shù)來構(gòu)造元素。
值得注意的是筷弦,標(biāo)準(zhǔn)庫不保證哪些算法可以與移動(dòng)迭代器一起使用肋演,哪些算法不能抑诸。因?yàn)橐苿?dòng)對象可以消除源對象,所以只有當(dāng)我們確信算法在分配給該元素或?qū)⒃撛貍鬟f給用戶定義的函數(shù)后才訪問該元素時(shí)爹殊,才應(yīng)將移動(dòng)迭代器傳遞給算法蜕乡。
因?yàn)橐苿?dòng)的對象具有不確定的狀態(tài),所以在對象上調(diào)用std::move
是一種危險(xiǎn)的操作梗夸。當(dāng)我們調(diào)用move
時(shí)层玲,我們必須絕對確定不能有其他用戶移動(dòng)對象。
在類代碼中明智地使用move
可以提供顯著的性能優(yōu)勢反症。隨便在普通用戶代碼中使用(與類實(shí)現(xiàn)代碼相對)辛块,移動(dòng)對象更有可能導(dǎo)致神秘且難以發(fā)現(xiàn)的錯(cuò)誤,而不是應(yīng)用程序性能的任何改進(jìn)铅碍。
在類實(shí)現(xiàn)代碼(例如移動(dòng)構(gòu)造函數(shù)或移動(dòng)賦值運(yùn)算符)之外润绵,只有當(dāng)我們確定需要執(zhí)行移動(dòng)并且保證移動(dòng)是安全的時(shí),才使用std::move
胞谈。
引用限定成員函數(shù)
我們以與定義const
成員函數(shù)相同的方式指示this
的左右值屬性尘盼。我們在參數(shù)列表后面放置一個(gè)引用限定符:
Code:
class Foo {
public:
Foo &operator=(const Foo&) &; // may assign only to modifiable lvalues
// other members of Foo
};
Foo &Foo::operator=(const Foo &rhs) &
{
// do whatever is needed to assign rhs to this object
return *this;
}
引用限定符可以是&
或&&
,表示這可以分別指向右值或左值。與const
限定符一樣,引用限定符只能出現(xiàn)在(非static
)成員函數(shù)上叹阔,并且必須出現(xiàn)在函數(shù)的聲明和定義中。
我們可以只在左值上運(yùn)行由&
限定的函數(shù)蝙搔,也可以只在右值上運(yùn)行由&&
限定的函數(shù):
Code:
Foo &retFoo(); // returns a reference; a call to retFoo is an lvalue
Foo retVal(); // returns by value; a call to retVal is an rvalue
Foo i, j; // i and j are lvalues
i = j; // ok: i is an lvalue
retFoo() = j; // ok: retFoo() returns an lvalue
retVal() = j; // error: retVal() returns an rvalue
i = retVal(); // ok: we can pass an rvalue as the right-hand operand to assignment
函數(shù)可以是const
和引用限定的。在這種情況下,引用限定符必須遵循const
限定符:
Code:
class Foo {
public:
Foo someMem() & const; // error: const qualifier must come first
Foo anotherMem() const &; // ok: const qualifier comes first
};
參考文獻(xiàn)
[1] Lippman S B , Josée Lajoie, Moo B E . C++ Primer (5th Edition)[J]. 2013.