自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Modern C++ 最核心的變化是什么?

開(kāi)發(fā) 后端
右值引用的意義通常解釋為兩大作用:移動(dòng)語(yǔ)義和完美轉(zhuǎn)發(fā)。本文主要討論移動(dòng)語(yǔ)義。

個(gè)人覺(jué)得最核心的變化是右值引用的引入,右值引用是  C++ 走向現(xiàn)代化的最重要一步。建議每一位 C++ 開(kāi)發(fā)者都應(yīng)該深入去了解并充分使用它。

右值引用是 C++11 中最重要的新特性之一,它解決了 C++ 中大量的歷史遺留問(wèn)題,使 C++ 標(biāo)準(zhǔn)庫(kù)的實(shí)現(xiàn)在多種場(chǎng)景下消除了不必要的額外開(kāi)銷(如 std::vector, std::string),也使得另外一些標(biāo)準(zhǔn)庫(kù)(如 std::unique_ptr, std::function)成為可能。即使你并不直接使用右值引用,也可以通過(guò)標(biāo)準(zhǔn)庫(kù),間接從這一新特性中受益。為了更好地理解標(biāo)準(zhǔn)庫(kù)結(jié)合右值引用帶來(lái)的優(yōu)化,我們有必要了解一下右值引用的重大意義。

右值引用的意義通常解釋為兩大作用:移動(dòng)語(yǔ)義和完美轉(zhuǎn)發(fā)。本文主要討論移動(dòng)語(yǔ)義。

一、移動(dòng)語(yǔ)義

移動(dòng)語(yǔ)義,簡(jiǎn)單來(lái)說(shuō)解決的是各種情形下對(duì)象的資源所有權(quán)轉(zhuǎn)移的問(wèn)題。而在 C++11 之前,移動(dòng)語(yǔ)義的缺失是 C++ 飽受詬病的問(wèn)題之一。

舉個(gè)例子。

問(wèn)題一:如何將大象放入冰箱?

答案是眾所周知的。首先你需要有一臺(tái)特殊的冰箱,這臺(tái)冰箱是為了裝下大象而制造的。你打開(kāi)冰箱門(mén),將大象放入冰箱,然后關(guān)上冰箱門(mén)。

問(wèn)題二:如何將大象從一臺(tái)冰箱轉(zhuǎn)移到另一臺(tái)冰箱?

普通解答:

打開(kāi)冰箱門(mén),取出大象,關(guān)上冰箱門(mén),打開(kāi)另一臺(tái)冰箱門(mén),放進(jìn)大象,關(guān)上冰箱門(mén)。

2B 解答:

在第二個(gè)冰箱中啟動(dòng)量子復(fù)制系統(tǒng),克隆一只完全相同的大象,然后啟動(dòng)高能激光將第一個(gè)冰箱內(nèi)的大象氣化消失。

等等,這個(gè) 2B 解答聽(tīng)起來(lái)很耳熟,這不就是 C++ 中要移動(dòng)一個(gè)對(duì)象時(shí)所做的事情嗎?

“移動(dòng)”,這是一個(gè)三歲小孩都明白的概念。將大象(資源)從一臺(tái)冰箱(對(duì)象)移動(dòng)到另一臺(tái)冰箱,這個(gè)行為是如此自然,沒(méi)有任何人會(huì)采用先復(fù)制大象,再銷毀大象這樣匪夷所思的方法。C++ 通過(guò)拷貝構(gòu)造函數(shù)和拷貝賦值操作符為類設(shè)計(jì)了拷貝/復(fù)制的概念,但為了實(shí)現(xiàn)對(duì)資源的移動(dòng)操作,調(diào)用者必須使用先復(fù)制、再析構(gòu)的方式。否則,就需要自己實(shí)現(xiàn)移動(dòng)資源的接口。

為了實(shí)現(xiàn)移動(dòng)語(yǔ)義,首先需要解決的問(wèn)題是,如何標(biāo)識(shí)對(duì)象的資源是可以被移動(dòng)的呢?這種機(jī)制必須以一種最低開(kāi)銷的方式實(shí)現(xiàn),并且對(duì)所有的類都有效。C++ 的設(shè)計(jì)者們注意到,大多數(shù)情況下,右值所包含的對(duì)象都是可以安全的被移動(dòng)的。

右值(相對(duì)應(yīng)的還有左值)是從 C 語(yǔ)言設(shè)計(jì)時(shí)就有的概念,但因?yàn)槠淙绱嘶A(chǔ),也是一個(gè)最常被忽略的概念。不嚴(yán)格的來(lái)說(shuō),左值對(duì)應(yīng)變量的存儲(chǔ)位置,而右值對(duì)應(yīng)變量的值本身。C++ 中右值可以被賦值給左值或者綁定到引用。類的右值是一個(gè)臨時(shí)對(duì)象,如果沒(méi)有被綁定到引用,在表達(dá)式結(jié)束時(shí)就會(huì)被廢棄。于是我們可以在右值被廢棄之前,移走它的資源進(jìn)行廢物利用,從而避免無(wú)意義的復(fù)制。被移走資源的右值在廢棄時(shí)已經(jīng)成為空殼,析構(gòu)的開(kāi)銷也會(huì)降低。

右值中的數(shù)據(jù)可以被安全移走這一特性使得右值被用來(lái)表達(dá)移動(dòng)語(yǔ)義。以同類型的右值構(gòu)造對(duì)象時(shí),需要以引用形式傳入?yún)?shù)。右值引用顧名思義專門(mén)用來(lái)引用右值,左值引用和右值引用可以被分別重載,這樣確保左值和右值分別調(diào)用到拷貝和移動(dòng)的兩種語(yǔ)義實(shí)現(xiàn)。對(duì)于左值,如果我們明確放棄對(duì)其資源的所有權(quán),則可以通過(guò)std::move()來(lái)將其轉(zhuǎn)為右值引用。std::move()實(shí)際上是 static_cast<T&&>() 的簡(jiǎn)單封裝。

右值引用至少可以解決以下場(chǎng)景中的移動(dòng)語(yǔ)義缺失問(wèn)題:

1.按值傳入?yún)?shù)

按值傳參是最符合人類思維的方式?;镜乃悸肥牵绻麄魅?yún)?shù)是為了將資源交給函數(shù)接受者,就應(yīng)該按值傳參。同時(shí),按值傳參可以兼容任何的 cv-qualified 左值、右值,是兼容性最好的方式。

class People {
public:
// 按值傳入字符串,可接收左值、右值。
// 接收左值時(shí)為復(fù)制,接收右值時(shí)為移動(dòng)
People(string name)
: name_(move(name)) // 顯式移動(dòng)構(gòu)造,將傳入的字符串移入成員變量
{
}
string name_;
};
People a("Alice"); // 移動(dòng)構(gòu)造name
string bn = "Bob";
People b(bn); // 拷貝構(gòu)造name

構(gòu)造a時(shí),調(diào)用了一次字符串的構(gòu)造函數(shù)和一次字符串的移動(dòng)構(gòu)造函數(shù)。如果使用 const string& name 接收參數(shù),那么會(huì)有一次構(gòu)造函數(shù)和一次拷貝構(gòu)造,以及一次 non-trivial 的析構(gòu)。盡管看起來(lái)很蛋疼,盡管編譯器還有優(yōu)化,但從語(yǔ)義來(lái)說(shuō)按值傳入?yún)?shù)是最優(yōu)的方式。

如果你要在構(gòu)造函數(shù)中接收 std::shared_ptr<X> 并且存入類的成員(這是非常常見(jiàn)的),那么按值傳入更是不二選擇??截?std::shared_ptr<X> 需要線程同步,相比之下移動(dòng) std::shared_ptr 是非常輕松愉快的。

2.按值返回

和接收輸入?yún)?shù)一樣,返回值按值返回也是最符合人類思維的方式。曾經(jīng)有無(wú)數(shù)函數(shù)為了返回容器而不得不寫(xiě)成這樣:

// 一個(gè)按值語(yǔ)義定義的字符串拆分函數(shù)
void str_split(const string& s, vector<string>* vec);

這里不考慮分隔符,假定分隔符是固定的。這樣要求 vec在外部被事先構(gòu)造,此時(shí)尚無(wú)從得知vec的大小。即使函數(shù)內(nèi)部有辦法預(yù)測(cè)vec的大小,因?yàn)楹瘮?shù)并不負(fù)責(zé)構(gòu)造vec,很可能仍需要 resize`。

對(duì)這樣的函數(shù)嵌套調(diào)用更是痛苦的事情,誰(shuí)用誰(shuí)知道啊。

有了移動(dòng)語(yǔ)義,就可以寫(xiě)成這樣:

vector<string> str_split(const string& s) {
vector<string> v;
// ...
return v; // v是左值,但優(yōu)先移動(dòng),不支持移動(dòng)時(shí)仍可復(fù)制。
}

如果函數(shù)按值返回,return 語(yǔ)句又直接返回了一個(gè)棧上的左值對(duì)象(輸入?yún)?shù)除外)時(shí),標(biāo)準(zhǔn)要求優(yōu)先調(diào)用移動(dòng)構(gòu)造函數(shù),如果不符再調(diào)用拷貝構(gòu)造函數(shù)。盡管 ``v是左值,仍然會(huì)優(yōu)先采用移動(dòng)語(yǔ)義,返回 vector` 從此變得云淡風(fēng)輕。此外,無(wú)論移動(dòng)或是拷貝,可能的情況下仍然適用編譯器優(yōu)化,但語(yǔ)義不受影響。

對(duì)于 std::unique_ptr 來(lái)說(shuō),這簡(jiǎn)直就是福音。

unique_ptr<SomeObj> create_obj(/*...*/) {
unique_ptr<SomeObj> ptr(new SomeObj(/*...*/));
ptr->foo(); // 一些可能的初始化
return ptr;
}

當(dāng)然還有更簡(jiǎn)單的形式:

unique_ptr<SomeObj> create_obj(/*...*/) {
return unique_ptr<SomeObj>(new SomeObj(/*...*/));
}

在工廠類中,這樣的語(yǔ)義是非常常見(jiàn)的。返回 unique_ptr 能夠明確對(duì)所構(gòu)造對(duì)象的所有權(quán)轉(zhuǎn)移,特別的,這樣的工廠類返回值可以被忽略而不會(huì)造成內(nèi)存泄露。上面兩種形式分別返回棧上的左值和右值,但都適用移動(dòng)語(yǔ)義(unique_ptr 不支持拷貝)。

3.接收右值表達(dá)式

沒(méi)有移動(dòng)語(yǔ)義時(shí),以表達(dá)式的值(例為函數(shù)調(diào)用)初始化對(duì)象或者給對(duì)象賦值是這樣的:

vector<string> str_split(const string& s);
// 返回的vector用以拷貝構(gòu)造對(duì)象v。為v申請(qǐng)堆內(nèi)存,復(fù)制數(shù)據(jù),然后析構(gòu)臨時(shí)對(duì)象(釋放堆內(nèi)存)。
vector<string> v = str_split("1,2,3");
vector<string> v2;
// 返回的vector被復(fù)制給對(duì)象v(拷貝賦值操作符)。需要先清理v2中原有數(shù)據(jù),將臨時(shí)對(duì)象中的數(shù)據(jù)復(fù)制給v2,然后析構(gòu)臨時(shí)對(duì)象。
v2 = str_split("1,2,3");
  •  注:v 的拷貝構(gòu)造調(diào)用有可能被優(yōu)化掉,盡管如此在語(yǔ)義上仍然是有一次拷貝操作。

同樣的代碼,在支持移動(dòng)語(yǔ)義的世界里就變得更美好了。

vector<string> str_split(const string& s);
// 返回的vector用以移動(dòng)構(gòu)造對(duì)象v。v直接取走臨時(shí)對(duì)象的堆上內(nèi)存,無(wú)需新申請(qǐng)。之后臨時(shí)對(duì)象成為空殼,不再擁有任何資源,析構(gòu)時(shí)也無(wú)需釋放堆內(nèi)存。
vector<string> v = str_split("1,2,3");
vector<string> v2;
// 返回的vector被移動(dòng)給對(duì)象v(移動(dòng)賦值操作符)。先釋放v2原有數(shù)據(jù),然后直接從返回值中取走數(shù)據(jù),然后返回值被析構(gòu)。
v2 = str_split("1,2,3");
  •  注:v 的移動(dòng)構(gòu)造調(diào)用有可能被優(yōu)化掉,盡管如此在語(yǔ)義上仍然是有一次移動(dòng)操作。

不用多說(shuō)也知道上面的形式是多么常用和自然。而且這里完全沒(méi)有任何對(duì)右值引用的顯式使用,性能提升卻默默的實(shí)現(xiàn)了。

4.對(duì)象存入容器

這個(gè)問(wèn)題和前面的構(gòu)造函數(shù)傳參是類似的。不同的是這里是按兩種引用分別傳參。參見(jiàn) std::vector 的 push_back 函數(shù)。

void push_back( const T& value ); // (1)
void push_back( T&& value ); // (2)

不用多說(shuō)自然是左值調(diào)用 1 右值調(diào)用 2。如果你要往容器內(nèi)放入超大對(duì)象,那么版本 2 自然是不 2 選擇。

vector<vector<string>> vv;
vector<string> v = {"123", "456"};
v.push_back("789"); // 臨時(shí)構(gòu)造的string類型右值被移動(dòng)進(jìn)容器v
vv.push_back(move(v)); // 顯式將v移動(dòng)進(jìn)vv

困擾多年的難言之隱是不是一洗了之了?

5.std::vector 的增長(zhǎng)

又一個(gè)隱蔽的優(yōu)化。當(dāng) vector 的存儲(chǔ)容量需要增長(zhǎng)時(shí),通常會(huì)重新申請(qǐng)一塊內(nèi)存,并把原來(lái)的內(nèi)容一個(gè)個(gè)復(fù)制過(guò)去并刪除。對(duì),復(fù)制并刪除,改用移動(dòng)就夠了。

對(duì)于像 vector<string> 這樣的容器,如果頻繁插入造成存儲(chǔ)容量不可避免的增長(zhǎng)時(shí),移動(dòng)語(yǔ)義可以帶來(lái)悄無(wú)聲息而且美好的優(yōu)化。

6.std::unique_ptr放入容器

曾經(jīng),由于 vector 增長(zhǎng)時(shí)會(huì)復(fù)制對(duì)象,像 std::unique_ptr 這樣不可復(fù)制的對(duì)象是無(wú)法放入容器的。但實(shí)際上 vector 并不復(fù)制對(duì)象,而只是“移動(dòng)”對(duì)象。所以隨著移動(dòng)語(yǔ)義的引入,std::unique_ptr 放入 std::vector 成為理所當(dāng)然的事情。

容器中存儲(chǔ) std::unique_ptr 有太多好處。想必每個(gè)人都寫(xiě)過(guò)這樣的代碼:

MyObj::MyObj() {
for (...) {
vec.push_back(new T());
}
// ...
}
MyObj::~MyObj() {
for (vector<T*>::iterator iter = vec.begin(); iter != vec.end(); ++iter) {
if (*iter) delete *iter;
}
// ...
}

繁瑣暫且不說(shuō),異常安全也是大問(wèn)題。使用 vector<unique_ptr<T>>,完全無(wú)需顯式析構(gòu),unqiue_ptr 自會(huì)打理一切。完全不用寫(xiě)析構(gòu)函數(shù)的感覺(jué),你造嗎?

unique_ptr 是非常輕量的封裝,存儲(chǔ)空間等價(jià)于裸指針,但安全性強(qiáng)了一個(gè)世紀(jì)。實(shí)際中需要共享所有權(quán)的對(duì)象(指針)是比較少的,但需要轉(zhuǎn)移所有權(quán)是非常常見(jiàn)的情況。auto_ptr 的失敗就在于其轉(zhuǎn)移所有權(quán)的繁瑣操作。unique_ptr 配合移動(dòng)語(yǔ)義即可輕松解決所有權(quán)傳遞的問(wèn)題。

  •  注:如果真的需要共享所有權(quán),那么基于引用計(jì)數(shù)的 shared_ptr 是一個(gè)好的選擇。shared_ptr 同樣可以移動(dòng)。由于不需要線程同步,移動(dòng) shared_ptr 比復(fù)制更輕量。

7.std::thread 的傳遞

thread 也是一種典型的不可復(fù)制的資源,但可以通過(guò)移動(dòng)來(lái)傳遞所有權(quán)。同樣 std::future std::promise std::packaged_task 等等這一票多線程類都是不可復(fù)制的,也都可以用移動(dòng)的方式傳遞。

二、完美轉(zhuǎn)發(fā)

除了移動(dòng)語(yǔ)義,右值引用還解決了 C++03 中引用語(yǔ)法無(wú)法轉(zhuǎn)發(fā)右值的問(wèn)題,實(shí)現(xiàn)了完美轉(zhuǎn)發(fā),才使得 std::function 能有一個(gè)優(yōu)雅的實(shí)現(xiàn)。這部分不再展開(kāi)了。

三、總結(jié)

移動(dòng)語(yǔ)義絕不是語(yǔ)法糖,而是帶來(lái)了 C++ 的深刻革新。移動(dòng)語(yǔ)義不僅僅是針對(duì)庫(kù)作者的,任何一個(gè)程序員都有必要去了解它。盡管你可能不會(huì)去主動(dòng)為自己的類實(shí)現(xiàn)移動(dòng)語(yǔ)義,但卻時(shí)時(shí)刻刻都在享受移動(dòng)語(yǔ)義帶來(lái)的受益。因此這絕不意味著這是一個(gè)可有可無(wú)的東西。

所以,如果你要寫(xiě)出優(yōu)雅的 Modern C++ 代碼,應(yīng)該多使用右值引用,喜歡它,擁抱它。

責(zé)任編輯:龐桂玉 來(lái)源: C語(yǔ)言與C++編程
相關(guān)推薦

2016-10-20 16:07:11

C++Modern C++異步

2022-02-16 12:52:22

C++項(xiàng)目編譯器

2010-02-05 14:51:48

C++托管

2019-07-17 13:41:36

VueReactJSX

2024-02-26 00:05:00

C++開(kāi)發(fā)

2010-01-25 18:05:40

C++語(yǔ)言

2018-04-04 14:29:33

2010-01-25 16:58:15

C++程序

2020-08-02 19:55:46

Python編程語(yǔ)言技術(shù)

2011-03-31 09:22:56

c++

2011-05-16 13:44:11

C++

2016-01-04 08:52:11

2022-08-02 12:12:07

勒索軟件API安全網(wǎng)絡(luò)漏洞

2020-03-10 11:00:22

CIO核心競(jìng)爭(zhēng)力競(jìng)爭(zhēng)力

2022-03-16 14:10:45

數(shù)字化轉(zhuǎn)型企業(yè)信息化

2015-12-07 10:09:40

程序員噩夢(mèng)

2015-12-04 08:49:00

程序員夢(mèng)魘

2012-07-17 10:54:21

大數(shù)據(jù)

2017-02-05 14:49:39

2015-08-26 09:54:19

物聯(lián)網(wǎng)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)