午夜视频在线网站,日韩视频精品在线,中文字幕精品一区二区三区在线,在线播放精品,1024你懂我懂的旧版人,欧美日韩一级黄色片,一区二区三区在线观看视频

分享

C++11常用新特性快速一覽

 蘭亭文藝 2020-02-06

最近工作中,遇到一些問題,使用C++11實(shí)現(xiàn)起來會(huì)更加方便,而線上的生產(chǎn)環(huán)境還不支持C++11,于是決定新年開工后,在組內(nèi)把C++11推廣開來,整理以下文檔,方便自己查閱,也方便同事快速上手。(對(duì)于異步編程十分實(shí)用的Future/Promise以及智能指針等,將不做整理介紹,組內(nèi)使用的框架已經(jīng)支持并廣泛使用了,用的是自己公司參考boost實(shí)現(xiàn)的版本)

1. nullptr

nullptr 出現(xiàn)的目的是為了替代 NULL。

在某種意義上來說,傳統(tǒng) C++ 會(huì)把 NULL0 視為同一種東西,這取決于編譯器如何定義 NULL,有些編譯器會(huì)將 NULL 定義為 ((void*)0),有些則會(huì)直接將其定義為 0。

C++ 不允許直接將 void * 隱式轉(zhuǎn)換到其他類型,但如果 NULL 被定義為 ((void*)0),那么當(dāng)編譯char *ch = NULL;時(shí),NULL 只好被定義為 0。

而這依然會(huì)產(chǎn)生問題,將導(dǎo)致了 C++ 中重載特性會(huì)發(fā)生混亂,考慮:

void foo(char *);

void foo(int);

對(duì)于這兩個(gè)函數(shù)來說,如果 NULL 又被定義為了 0 那么 foo(NULL); 這個(gè)語句將會(huì)去調(diào)用 foo(int),從而導(dǎo)致代碼違反直觀。

為了解決這個(gè)問題,C++11 引入了 nullptr 關(guān)鍵字,專門用來區(qū)分空指針、0。

nullptr 的類型為 nullptr_t,能夠隱式的轉(zhuǎn)換為任何指針或成員指針的類型,也能和他們進(jìn)行相等或者不等的比較。

當(dāng)需要使用 NULL 時(shí)候,養(yǎng)成直接使用 nullptr的習(xí)慣。

2. 類型推導(dǎo)

C++11 引入了 auto decltype 這兩個(gè)關(guān)鍵字實(shí)現(xiàn)了類型推導(dǎo),讓編譯器來操心變量的類型。

auto

auto 在很早以前就已經(jīng)進(jìn)入了 C++,但是他始終作為一個(gè)存儲(chǔ)類型的指示符存在,與 register 并存。在傳統(tǒng) C++ 中,如果一個(gè)變量沒有聲明為 register 變量,將自動(dòng)被視為一個(gè) auto 變量。而隨著 register 被棄用,對(duì) auto 的語義變更也就非常自然了。

使用 auto 進(jìn)行類型推導(dǎo)的一個(gè)最為常見而且顯著的例子就是迭代器。在以前我們需要這樣來書寫一個(gè)迭代器:

for(vector<int>::const_iterator itr = vec.cbegin(); itr != vec.cend(); ++itr)

而有了 auto 之后可以:

// 由于 cbegin() 將返回 vector<int>::const_iterator

// 所以 itr 也應(yīng)該是 vector<int>::const_iterator 類型

for(auto itr = vec.cbegin(); itr != vec.cend(); ++itr);

一些其他的常見用法:

auto i = 5;             // i 被推導(dǎo)為 int

auto arr = new auto(10) // arr 被推導(dǎo)為 int *

注意:auto 不能用于函數(shù)傳參,因此下面的做法是無法通過編譯的(考慮重載的問題,我們應(yīng)該使用模板):

int add(auto x, auto y);

此外,auto 還不能用于推導(dǎo)數(shù)組類型:

#include <iostream>

int main() {

 auto i = 5;

 int arr[10] = {0};

 auto auto_arr = arr;

 auto auto_arr2[10] = arr;

 return 0;

}

decltype

decltype 關(guān)鍵字是為了解決 auto 關(guān)鍵字只能對(duì)變量進(jìn)行類型推導(dǎo)的缺陷而出現(xiàn)的。它的用法和 sizeof 很相似:

decltype(表達(dá)式)

1

在此過程中,編譯器分析表達(dá)式并得到它的類型,卻不實(shí)際計(jì)算表達(dá)式的值。

有時(shí)候,我們可能需要計(jì)算某個(gè)表達(dá)式的類型,例如:

auto x = 1;

auto y = 2;

decltype(x+y) z;

拖尾返回類型、auto decltype 配合

你可能會(huì)思考,auto 能不能用于推導(dǎo)函數(shù)的返回類型。考慮這樣一個(gè)例子加法函數(shù)的例子,在傳統(tǒng) C++ 中我們必須這么寫:

template<typename R, typename T, typename U>

R add(T x, U y) {

    return x+y

}

這樣的代碼其實(shí)變得很丑陋,因?yàn)槌绦騿T在使用這個(gè)模板函數(shù)的時(shí)候,必須明確指出返回類型。但事實(shí)上我們并不知道 add() 這個(gè)函數(shù)會(huì)做什么樣的操作,獲得一個(gè)什么樣的返回類型。

C++11 中這個(gè)問題得到解決。雖然你可能馬上回反應(yīng)出來使用 decltype 推導(dǎo) x+y 的類型,寫出這樣的代碼:

decltype(x+y) add(T x, U y);

1

但事實(shí)上這樣的寫法并不能通過編譯。這是因?yàn)樵诰幾g器讀到 decltype(x+y) 時(shí),x y 尚未被定義。為了解決這個(gè)問題,C++11 還引入了一個(gè)叫做拖尾返回類型(trailing return type),利用 auto 關(guān)鍵字將返回類型后置:

template<typename T, typename U>

auto add(T x, U y) -> decltype(x+y) {

    return x+y;

}

C++14 開始是可以直接讓普通函數(shù)具備返回值推導(dǎo),因此下面的寫法變得合法:

template<typename T, typename U>

auto add(T x, U y) {

    return x+y;

}

3. 區(qū)間迭代

基于范圍的 for 循環(huán)

C++11 引入了基于范圍的迭代寫法,我們擁有了能夠?qū)懗鱿?/span> Python 一樣簡潔的循環(huán)語句。

最常用的 std::vector 遍歷將從原來的樣子:

std::vector<int> arr(5, 100);

for(std::vector<int>::iterator i = arr.begin(); i != arr.end(); ++i) {

    std::cout << *i << std::endl;

}

變得非常的簡單:

// & 啟用了引用

for(auto &i : arr) {   

    std::cout << i << std::endl;

}

4. 初始化列表

C++11 提供了統(tǒng)一的語法來初始化任意的對(duì)象,例如:

struct A {

    int a;

    float b;

};

struct B {

    B(int _a, float _b): a(_a), b(_b) {}

private:

    int a;

    float b;

};

A a {1, 1.1};    // 統(tǒng)一的初始化語法

B b {2, 2.2};

C++11 還把初始化列表的概念綁定到了類型上,并將其稱之為 std::initializer_list,允許構(gòu)造函數(shù)或其他函數(shù)像參數(shù)一樣使用初始化列表,這就為類對(duì)象的初始化與普通數(shù)組和 POD 的初始化方法提供了統(tǒng)一的橋梁,例如:

#include <initializer_list>

class Magic {

public:

    Magic(std::initializer_list<int> list) {}

};

Magic magic = {1,2,3,4,5};

std::vector<int> v = {1, 2, 3, 4};

5. 模板增強(qiáng)

外部模板

傳統(tǒng) C++ 中,模板只有在使用時(shí)才會(huì)被編譯器實(shí)例化。只要在每個(gè)編譯單元(文件)中編譯的代碼中遇到了被完整定義的模板,都會(huì)實(shí)例化。這就產(chǎn)生了重復(fù)實(shí)例化而導(dǎo)致的編譯時(shí)間的增加。并且,我們沒有辦法通知編譯器不要觸發(fā)模板實(shí)例化。

C++11 引入了外部模板,擴(kuò)充了原來的強(qiáng)制編譯器在特定位置實(shí)例化模板的語法,使得能夠顯式的告訴編譯器何時(shí)進(jìn)行模板的實(shí)例化:

template class std::vector<bool>;            // 強(qiáng)行實(shí)例化

extern template class std::vector<double>;  // 不在該編譯文件中實(shí)例化模板

尖括號(hào) >

在傳統(tǒng) C++ 的編譯器中,>>一律被當(dāng)做右移運(yùn)算符來進(jìn)行處理。但實(shí)際上我們很容易就寫出了嵌套模板的代碼:

std::vector<std::vector<int>> wow;

這在傳統(tǒng)C++編譯器下是不能夠被編譯的,而 C++11 開始,連續(xù)的右尖括號(hào)將變得合法,并且能夠順利通過編譯。

類型別名模板

在傳統(tǒng) C++中,typedef 可以為類型定義一個(gè)新的名稱,但是卻沒有辦法為模板定義一個(gè)新的名稱。因?yàn)?,模板不是類型。例如?/span>

template< typename T, typename U, int value>

class SuckType {

public:

    T a;

    U b;

    SuckType():a(value),b(value){}

};

template< typename U>

typedef SuckType<std::vector<int>, U, 1> NewType; // 不合法

C++11 使用 using 引入了下面這種形式的寫法,并且同時(shí)支持對(duì)傳統(tǒng) typedef 相同的功效:

template <typename T>

using NewType = SuckType<int, T, 1>;    // 合法

默認(rèn)模板參數(shù)

我們可能定義了一個(gè)加法函數(shù):

template<typename T, typename U>

auto add(T x, U y) -> decltype(x+y) {

    return x+y

}

但在使用時(shí)發(fā)現(xiàn),要使用 add,就必須每次都指定其模板參數(shù)的類型。

C++11 中提供了一種便利,可以指定模板的默認(rèn)參數(shù):

template<typename T = int, typename U = int>

auto add(T x, U y) -> decltype(x+y) {

    return x+y;

}

6. 構(gòu)造函數(shù)

委托構(gòu)造

C++11 引入了委托構(gòu)造的概念,這使得構(gòu)造函數(shù)可以在同一個(gè)類中一個(gè)構(gòu)造函數(shù)調(diào)用另一個(gè)構(gòu)造函數(shù),從而達(dá)到簡化代碼的目的:

class Base {

public:

    int value1;

    int value2;

    Base() {

        value1 = 1;

    }

    Base(int value) : Base() {  // 委托 Base() 構(gòu)造函數(shù)

        value2 = 2;

    }

};

繼承構(gòu)造

在繼承體系中,如果派生類想要使用基類的構(gòu)造函數(shù),需要在構(gòu)造函數(shù)中顯式聲明。

假若基類擁有為數(shù)眾多的不同版本的構(gòu)造函數(shù),這樣,在派生類中得寫很多對(duì)應(yīng)的“透?jìng)鳌睒?gòu)造函數(shù)。如下:

struct A

{

  A(int i) {}

  A(double d,int i){}

  A(float f,int i,const char* c){}

  //...等等系列的構(gòu)造函數(shù)版本

};

struct B:A

{

  B(int i):A(i){}

  B(double d,int i):A(d,i){}

  B(folat f,int i,const char* c):A(f,i,e){}

  //......等等好多個(gè)和基類構(gòu)造函數(shù)對(duì)應(yīng)的構(gòu)造函數(shù)

};

C++11的繼承構(gòu)造:

struct A

{

  A(int i) {}

  A(double d,int i){}

  A(float f,int i,const char* c){}

  //...等等系列的構(gòu)造函數(shù)版本

}

struct B:A

{

  using A::A;

  //關(guān)于基類各構(gòu)造函數(shù)的繼承一句話搞定

  //......

};

如果一個(gè)繼承構(gòu)造函數(shù)不被相關(guān)的代碼使用,編譯器不會(huì)為之產(chǎn)生真正的函數(shù)代碼,這樣比透?jìng)骰惛鞣N構(gòu)造函數(shù)更加節(jié)省目標(biāo)代碼空間。

7. Lambda 表達(dá)式

Lambda 表達(dá)式,實(shí)際上就是提供了一個(gè)類似匿名函數(shù)的特性,而匿名函數(shù)則是在需要一個(gè)函數(shù),但是又不想費(fèi)力去命名一個(gè)函數(shù)的情況下去使用的。

Lambda 表達(dá)式的基本語法如下:

[ caputrue ] ( params ) opt -> ret { body; };

1

1) capture是捕獲列表;

2) params是參數(shù)表;(選填)

3) opt是函數(shù)選項(xiàng);可以填mutable,exception,attribute(選填)

mutable說明lambda表達(dá)式體內(nèi)的代碼可以修改被捕獲的變量,并且可以訪問被捕獲的對(duì)象的non-const方法。

exception說明lambda表達(dá)式是否拋出異常以及何種異常。

attribute用來聲明屬性。

4) ret是返回值類型(拖尾返回類型)。(選填)

5) body是函數(shù)體。

捕獲列表:lambda表達(dá)式的捕獲列表精細(xì)控制了lambda表達(dá)式能夠訪問的外部變量,以及如何訪問這些變量。

1) []不捕獲任何變量。

2) [&]捕獲外部作用域中所有變量,并作為引用在函數(shù)體中使用(按引用捕獲)。

3) [=]捕獲外部作用域中所有變量,并作為副本在函數(shù)體中使用(按值捕獲)。注意值捕獲的前提是變量可以拷貝,且被捕獲的變量在 lambda 表達(dá)式被創(chuàng)建時(shí)拷貝,而非調(diào)用時(shí)才拷貝。如果希望lambda表達(dá)式在調(diào)用時(shí)能即時(shí)訪問外部變量,我們應(yīng)當(dāng)使用引用方式捕獲。

int a = 0;

auto f = [=] { return a; };

a+=1;

cout << f() << endl;       //輸出0

int a = 0;

auto f = [&a] { return a; };

a+=1;

cout << f() <<endl;       //輸出1

4) [=,&foo]按值捕獲外部作用域中所有變量,并按引用捕獲foo變量。

5) [bar]按值捕獲bar變量,同時(shí)不捕獲其他變量。

6) [this]捕獲當(dāng)前類中的this指針,讓lambda表達(dá)式擁有和當(dāng)前類成員函數(shù)同樣的訪問權(quán)限。如果已經(jīng)使用了&或者=,就默認(rèn)添加此選項(xiàng)。捕獲this的目的是可以在lamda中使用當(dāng)前類的成員函數(shù)和成員變量。

class A

{

 public:

     int i_ = 0;

     void func(int x,int y){

         auto x1 = [] { return i_; };                   //error,沒有捕獲外部變量

         auto x2 = [=] { return i_ + x + y; };          //OK

         auto x3 = [&] { return i_ + x + y; };        //OK

         auto x4 = [this] { return i_; };               //OK

         auto x5 = [this] { return i_ + x + y; };       //error,沒有捕獲x,y

         auto x6 = [this, x, y] { return i_ + x + y; };     //OK

         auto x7 = [this] { return i_++; };             //OK

};

int a=0 , b=1;

auto f1 = [] { return a; };                         //error,沒有捕獲外部變量   

auto f2 = [&] { return a++ };                      //OK

auto f3 = [=] { return a; };                        //OK

auto f4 = [=] {return a++; };                       //error,a是以復(fù)制方式捕獲的,無法修改

auto f5 = [a] { return a+b; };                      //error,沒有捕獲變量b

auto f6 = [a, &b] { return a + (b++); };                //OK

auto f7 = [=, &b] { return a + (b++); };                //OK

注意f4,雖然按值捕獲的變量值均復(fù)制一份存儲(chǔ)在lambda表達(dá)式變量中,修改他們也并不會(huì)真正影響到外部,但我們卻仍然無法修改它們。如果希望去修改按值捕獲的外部變量,需要顯示指明lambda表達(dá)式為mutable。被mutable修飾的lambda表達(dá)式就算沒有參數(shù)也要寫明參數(shù)列表。

原因:lambda表達(dá)式可以說是就地定義仿函數(shù)閉包的“語法糖”。它的捕獲列表捕獲住的任何外部變量,最終會(huì)變?yōu)殚]包類型的成員變量。按照C++標(biāo)準(zhǔn),lambda表達(dá)式的operator()默認(rèn)是const的,一個(gè)const成員函數(shù)是無法修改成員變量的值的。而mutable的作用,就在于取消operator()const

int a = 0;

auto f1 = [=] { return a++; };                //error

auto f2 = [=] () mutable { return a++; };       //OK

lambda表達(dá)式的大致原理:每當(dāng)你定義一個(gè)lambda表達(dá)式后,編譯器會(huì)自動(dòng)生成一個(gè)匿名類(這個(gè)類重載了()運(yùn)算符),我們稱為閉包類型(closure type)。那么在運(yùn)行時(shí),這個(gè)lambda表達(dá)式就會(huì)返回一個(gè)匿名的閉包實(shí)例,是一個(gè)右值。所以,我們上面的lambda表達(dá)式的結(jié)果就是一個(gè)個(gè)閉包。對(duì)于復(fù)制傳值捕捉方式,類中會(huì)相應(yīng)添加對(duì)應(yīng)類型的非靜態(tài)數(shù)據(jù)成員。在運(yùn)行時(shí),會(huì)用復(fù)制的值初始化這些成員變量,從而生成閉包。對(duì)于引用捕獲方式,無論是否標(biāo)記mutable,都可以在lambda表達(dá)式中修改捕獲的值。至于閉包類中是否有對(duì)應(yīng)成員,C++標(biāo)準(zhǔn)中給出的答案是:不清楚的,與具體實(shí)現(xiàn)有關(guān)。

lambda表達(dá)式是不能被賦值的:

auto a = [] { cout << "A" << endl; };

auto b = [] { cout << "B" << endl; };

a = b;   // 非法,lambda無法賦值

auto c = a;   // 合法,生成一個(gè)副本

閉包類型禁用了賦值操作符,但是沒有禁用復(fù)制構(gòu)造函數(shù),所以你仍然可以用一個(gè)lambda表達(dá)式去初始化另外一個(gè)lambda表達(dá)式而產(chǎn)生副本。

在多種捕獲方式中,最好不要使用[=][&]默認(rèn)捕獲所有變量。

默認(rèn)引用捕獲所有變量,你有很大可能會(huì)出現(xiàn)懸掛引用(Dangling references),因?yàn)橐貌东@不會(huì)延長引用的變量的生命周期:

std::function<int(int)> add_x(int x)

{

    return [&](int a) { return x + a; };

}

上面函數(shù)返回了一個(gè)lambda表達(dá)式,參數(shù)x僅是一個(gè)臨時(shí)變量,函數(shù)add_x調(diào)用后就被銷毀了,但是返回的lambda表達(dá)式卻引用了該變量,當(dāng)調(diào)用這個(gè)表達(dá)式時(shí),引用的是一個(gè)垃圾值,會(huì)產(chǎn)生沒有意義的結(jié)果。上面這種情況,使用默認(rèn)傳值方式可以避免懸掛引用問題。

但是采用默認(rèn)值捕獲所有變量仍然有風(fēng)險(xiǎn),看下面的例子:

class Filter

{

public:

    Filter(int divisorVal):

        divisor{divisorVal}

    {}

    std::function<bool(int)> getFilter()

    {

        return [=](int value) {return value % divisor == 0; };

    }

private:

    int divisor;

};

這個(gè)類中有一個(gè)成員方法,可以返回一個(gè)lambda表達(dá)式,這個(gè)表達(dá)式使用了類的數(shù)據(jù)成員divisor。而且采用默認(rèn)值方式捕捉所有變量。你可能認(rèn)為這個(gè)lambda表達(dá)式也捕捉了divisor的一份副本,但是實(shí)際上并沒有。因?yàn)閿?shù)據(jù)成員divisor對(duì)lambda表達(dá)式并不可見,你可以用下面的代碼驗(yàn)證:

// 類的方法,下面無法編譯,因?yàn)?/span>divisor并不在lambda捕捉的范圍

std::function<bool(int)> getFilter()

{

    return [divisor](int value) {return value % divisor == 0; };

}

原代碼中,lambda表達(dá)式實(shí)際上捕捉的是this指針的副本,所以原來的代碼等價(jià)于:

std::function<bool(int)> getFilter()

{

    return [this](int value) {return value % this->divisor == 0; };

}

盡管還是以值方式捕獲,但是捕獲的是指針,其實(shí)相當(dāng)于以引用的方式捕獲了當(dāng)前類對(duì)象,所以lambda表達(dá)式的閉包與一個(gè)類對(duì)象綁定在一起了,這很危險(xiǎn),因?yàn)槟闳匀挥锌赡茉陬悓?duì)象析構(gòu)后使用這個(gè)lambda表達(dá)式,那么類似“懸掛引用”的問題也會(huì)產(chǎn)生。所以,采用默認(rèn)值捕捉所有變量仍然是不安全的,主要是由于指針變量的復(fù)制,實(shí)際上還是按引用傳值。

lambda表達(dá)式可以賦值給對(duì)應(yīng)類型的函數(shù)指針。但是使用函數(shù)指針并不是那么方便。所以STL定義在< functional >頭文件提供了一個(gè)多態(tài)的函數(shù)對(duì)象封裝std::function,其類似于函數(shù)指針。它可以綁定任何類函數(shù)對(duì)象,只要參數(shù)與返回類型相同。如下面的返回一個(gè)bool且接收兩個(gè)int的函數(shù)包裝器:

std::function<bool(int, int)> wrapper = [](int x, int y) { return x < y; };

1

lambda表達(dá)式一個(gè)更重要的應(yīng)用是其可以用于函數(shù)的參數(shù),通過這種方式可以實(shí)現(xiàn)回調(diào)函數(shù)。

最常用的是在STL算法中,比如你要統(tǒng)計(jì)一個(gè)數(shù)組中滿足特定條件的元素?cái)?shù)量,通過lambda表達(dá)式給出條件,傳遞給count_if函數(shù):

int value = 3;

vector<int> v {1, 3, 5, 2, 6, 10};

int count = std::count_if(v.beigin(), v.end(), [value](int x) { return x > value; });

再比如你想生成斐波那契數(shù)列,然后保存在數(shù)組中,此時(shí)你可以使用generate函數(shù),并輔助lambda表達(dá)式:

vector<int> v(10);

int a = 0;

int b = 1;

std::generate(v.begin(), v.end(), [&a, &b] { int value = b; b = b + a; a = value; return value; });

// 此時(shí)v {1, 1, 2, 3, 5, 8, 13, 21, 34, 55}

當(dāng)需要遍歷容器并對(duì)每個(gè)元素進(jìn)行操作時(shí):

std::vector<int> v = { 1, 2, 3, 4, 5, 6 };

int even_count = 0;

for_each(v.begin(), v.end(), [&even_count](int val){

    if(!(val & 1)){

        ++ even_count;

    }

});

std::cout << "The number of even is " << even_count << std::endl;

大部分STL算法,可以非常靈活地搭配lambda表達(dá)式來實(shí)現(xiàn)想要的效果。

8. 新增容器

std::array

std::array 保存在棧內(nèi)存中,相比堆內(nèi)存中的 std::vector,我們能夠靈活的訪問這里面的元素,從而獲得更高的性能。

std::array 會(huì)在編譯時(shí)創(chuàng)建一個(gè)固定大小的數(shù)組,std::array 不能夠被隱式的轉(zhuǎn)換成指針,使用 std::array只需指定其類型和大小即可:

std::array<int, 4> arr= {1,2,3,4};

int len = 4;

std::array<int, len> arr = {1,2,3,4}; // 非法, 數(shù)組大小參數(shù)必須是常量表達(dá)式

當(dāng)我們開始用上了 std::array 時(shí),難免會(huì)遇到要將其兼容 C 風(fēng)格的接口,這里有三種做法:

void foo(int *p, int len) {

    return;

}

std::array<int 4> arr = {1,2,3,4};

// C 風(fēng)格接口傳參

// foo(arr, arr.size());           // 非法, 無法隱式轉(zhuǎn)換

foo(&arr[0], arr.size());

foo(arr.data(), arr.size());

// 使用 `std::sort`

std::sort(arr.begin(), arr.end());

std::forward_list

std::forward_list 是一個(gè)列表容器,使用方法和 std::list 基本類似。

std::list 的雙向鏈表的實(shí)現(xiàn)不同,std::forward_list 使用單向鏈表進(jìn)行實(shí)現(xiàn),提供了 O(1) 復(fù)雜度的元素插入,不支持快速隨機(jī)訪問(這也是鏈表的特點(diǎn)),也是標(biāo)準(zhǔn)庫容器中唯一一個(gè)不提供 size() 方法的容器。當(dāng)不需要雙向迭代時(shí),具有比 std::list 更高的空間利用率。

無序容器

C++11 引入了兩組無序容器:

std::unordered_map/std::unordered_multimap std::unordered_set/std::unordered_multiset。

無序容器中的元素是不進(jìn)行排序的,內(nèi)部通過 Hash 表實(shí)現(xiàn),插入和搜索元素的平均復(fù)雜度為 O(constant)。

元組 std::tuple

元組的使用有三個(gè)核心的函數(shù):

std::make_tuple: 構(gòu)造元組

std::get: 獲得元組某個(gè)位置的值

std::tie: 元組拆包

#include <tuple>

#include <iostream>

auto get_student(int id)

{

    // 返回類型被推斷為 std::tuple<double, char, std::string>

    if (id == 0)

        return std::make_tuple(3.8, 'A', "張三");

    if (id == 1)

        return std::make_tuple(2.9, 'C', "李四");

    if (id == 2)

        return std::make_tuple(1.7, 'D', "王五");

    return std::make_tuple(0.0, 'D', "null");  

    // 如果只寫 0 會(huì)出現(xiàn)推斷錯(cuò)誤, 編譯失敗

}

int main()

{

    auto student = get_student(0);

    std::cout << "ID: 0, "

    << "GPA: " << std::get<0>(student) << ", "

    << "成績: " << std::get<1>(student) << ", "

    << "姓名: " << std::get<2>(student) << '\n';

    double gpa;

    char grade;

    std::string name;

    // 元組進(jìn)行拆包

    std::tie(gpa, grade, name) = get_student(1);

    std::cout << "ID: 1, "

    << "GPA: " << gpa << ", "

    << "成績: " << grade << ", "

    << "姓名: " << name << '\n';

}

合并兩個(gè)元組,可以通過 std::tuple_cat 來實(shí)現(xiàn)。

auto new_tuple = std::tuple_cat(get_student(1), std::move(t));

9. 正則表達(dá)式

正則表達(dá)式描述了一種字符串匹配的模式。一般使用正則表達(dá)式主要是實(shí)現(xiàn)下面三個(gè)需求:

1) 檢查一個(gè)串是否包含某種形式的子串;

2) 將匹配的子串替換;

3) 從某個(gè)串中取出符合條件的子串。

C++11 提供的正則表達(dá)式庫操作 std::string 對(duì)象,對(duì)模式 std::regex (本質(zhì)是 std::basic_regex)進(jìn)行初始化,通過 std::regex_match 進(jìn)行匹配,從而產(chǎn)生 std::smatch (本質(zhì)是 std::match_results 對(duì)象)。

我們通過一個(gè)簡單的例子來簡單介紹這個(gè)庫的使用。考慮下面的正則表達(dá)式:

[a-z]+.txt: 在這個(gè)正則表達(dá)式中, [a-z] 表示匹配一個(gè)小寫字母, + 可以使前面的表達(dá)式匹配多次,因此 [a-z]+ 能夠匹配一個(gè)及以上小寫字母組成的字符串。在正則表達(dá)式中一個(gè) . 表示匹配任意字符,而 . 轉(zhuǎn)義后則表示匹配字符 . ,最后的 txt 表示嚴(yán)格匹配 txt 這三個(gè)字母。因此這個(gè)正則表達(dá)式的所要匹配的內(nèi)容就是文件名為純小寫字母的文本文件。

std::regex_match 用于匹配字符串和正則表達(dá)式,有很多不同的重載形式。最簡單的一個(gè)形式就是傳入std::string 以及一個(gè) std::regex 進(jìn)行匹配,當(dāng)匹配成功時(shí),會(huì)返回 true,否則返回 false。例如:

#include <iostream>

#include <string>

#include <regex>

int main() {

    std::string fnames[] = {"foo.txt", "bar.txt", "test", "a0.txt", "AAA.txt"};

    // C++ `\` 會(huì)被作為字符串內(nèi)的轉(zhuǎn)義符,為使 `\.` 作為正則表達(dá)式傳遞進(jìn)去生效,需要對(duì) `\` 進(jìn)行二次轉(zhuǎn)義,從而有 `\\.`

    std::regex txt_regex("[a-z]+\\.txt");

    for (const auto &fname: fnames)

        std::cout << fname << ": " << std::regex_match(fname, txt_regex) << std::endl;

}

另一種常用的形式就是依次傳入 std::string/std::smatch/std::regex 三個(gè)參數(shù),其中 std::smatch 的本質(zhì)其實(shí)是 std::match_results,在標(biāo)準(zhǔn)庫中, std::smatch 被定義為了 std::match_results,也就是一個(gè)子串迭代器類型的 match_results。使用 std::smatch 可以方便的對(duì)匹配的結(jié)果進(jìn)行獲取,例如:

std::regex base_regex("([a-z]+)\\.txt");

std::smatch base_match;

for(const auto &fname: fnames) {

    if (std::regex_match(fname, base_match, base_regex)) {

        // sub_match 的第一個(gè)元素匹配整個(gè)字符串

        // sub_match 的第二個(gè)元素匹配了第一個(gè)括號(hào)表達(dá)式

        if (base_match.size() == 2) {

            std::string base = base_match[1].str();

            std::cout << "sub-match[0]: " << base_match[0].str() << std::endl;

            std::cout << fname << " sub-match[1]: " << base << std::endl;

        }

    }

}

以上兩個(gè)代碼段的輸出結(jié)果為:

foo.txt: 1

bar.txt: 1

test: 0

a0.txt: 0

AAA.txt: 0

sub-match[0]: foo.txt

foo.txt sub-match[1]: foo

sub-match[0]: bar.txt

bar.txt sub-match[1]: bar

10. 語言級(jí)線程支持

std::thread

std::mutex/std::unique_lock

std::future/std::packaged_task

std::condition_variable

代碼編譯需要使用 -pthread 選項(xiàng)

11. 右值引用和move語義

先看一個(gè)簡單的例子直觀感受下:

string a(x);                                    // line 1

string b(x + y);                                    // line 2

string c(some_function_returning_a_string());       // line 3

如果使用以下拷貝構(gòu)造函數(shù):

string(const string& that)

{

    size_t size = strlen(that.data) + 1;

    data = new char[size];

    memcpy(data, that.data, size);

}

以上3行中,只有第一行(line 1)x深度拷貝是有必要的,因?yàn)槲覀兛赡軙?huì)在后邊用到x,x是一個(gè)左值(lvalues)

第二行和第三行的參數(shù)則是右值,因?yàn)楸磉_(dá)式產(chǎn)生的string對(duì)象是匿名對(duì)象,之后沒有辦法再使用了。

C++ 11引入了一種新的機(jī)制叫做“右值引用”,以便我們通過重載直接使用右值參數(shù)。我們所要做的就是寫一個(gè)以右值引用為參數(shù)的構(gòu)造函數(shù):

string(string&& that)   // string&& is an rvalue reference to a string

{

data = that.data;

that.data = 0;

}

我們沒有深度拷貝堆內(nèi)存中的數(shù)據(jù),而是僅僅復(fù)制了指針,并把源對(duì)象的指針置空。事實(shí)上,我們“偷取”了屬于源對(duì)象的內(nèi)存數(shù)據(jù)。由于源對(duì)象是一個(gè)右值,不會(huì)再被使用,因此客戶并不會(huì)覺察到源對(duì)象被改變了。在這里,我們并沒有真正的復(fù)制,所以我們把這個(gè)構(gòu)造函數(shù)叫做“轉(zhuǎn)移構(gòu)造函數(shù)”(move constructor),他的工作就是把資源從一個(gè)對(duì)象轉(zhuǎn)移到另一個(gè)對(duì)象,而不是復(fù)制他們。

有了右值引用,再來看看賦值操作符:

string& operator=(string that)

{

std::swap(data, that.data);

return *this;

}

注意到我們是直接對(duì)參數(shù)that傳值,所以that會(huì)像其他任何對(duì)象一樣被初始化,那么確切的說,that是怎樣被初始化的呢?對(duì)于C++ 98,答案是復(fù)制構(gòu)造函數(shù),但是對(duì)于C++ 11,編譯器會(huì)依據(jù)參數(shù)是左值還是右值在復(fù)制構(gòu)造函數(shù)和轉(zhuǎn)移構(gòu)造函數(shù)間進(jìn)行選擇。

如果是a=b,這樣就會(huì)調(diào)用復(fù)制構(gòu)造函數(shù)來初始化that(因?yàn)?/span>b是左值),賦值操作符會(huì)與新創(chuàng)建的對(duì)象交換數(shù)據(jù),深度拷貝。這就是copy and swap 慣用法的定義:構(gòu)造一個(gè)副本,與副本交換數(shù)據(jù),并讓副本在作用域內(nèi)自動(dòng)銷毀。這里也一樣。

如果是a = x + y,這樣就會(huì)調(diào)用轉(zhuǎn)移構(gòu)造函數(shù)來初始化that(因?yàn)?/span>x+y是右值),所以這里沒有深度拷貝,只有高效的數(shù)據(jù)轉(zhuǎn)移。相對(duì)于參數(shù),that依然是一個(gè)獨(dú)立的對(duì)象,但是他的構(gòu)造函數(shù)是無用的(trivial),因此堆中的數(shù)據(jù)沒有必要復(fù)制,而僅僅是轉(zhuǎn)移。沒有必要復(fù)制他,因?yàn)?/span>x+y是右值,再次,從右值指向的對(duì)象中轉(zhuǎn)移是沒有問題的。

總結(jié)一下:復(fù)制構(gòu)造函數(shù)執(zhí)行的是深度拷貝,因?yàn)樵磳?duì)象本身必須不能被改變。而轉(zhuǎn)移構(gòu)造函數(shù)卻可以復(fù)制指針,把源對(duì)象的指針置空,這種形式下,這是安全的,因?yàn)橛脩舨豢赡茉偈褂眠@個(gè)對(duì)象了。

下面我們進(jìn)一步討論右值引用和move語義。

C++98標(biāo)準(zhǔn)庫中提供了一種唯一擁有性的智能指針std::auto_ptr,該類型在C++11中已被廢棄,因?yàn)槠洹皬?fù)制”行為是危險(xiǎn)的。

auto_ptr<Shape> a(new Triangle);

auto_ptr<Shape> b(a);

注意b是怎樣使用a進(jìn)行初始化的,它不復(fù)制triangle,而是把triangle的所有權(quán)從a傳遞給了b,也可以說成“a 被轉(zhuǎn)移進(jìn)了b”或者“triangle被從a轉(zhuǎn)移到了b”。

auto_ptr 的復(fù)制構(gòu)造函數(shù)可能看起來像這樣(簡化):

auto_ptr(auto_ptr& source)   // note the missing const

{

p = source.p;

source.p = 0;   // now the source no longer owns the object

}

auto_ptr 的危險(xiǎn)之處在于看上去應(yīng)該是復(fù)制,但實(shí)際上確是轉(zhuǎn)移。調(diào)用被轉(zhuǎn)移過的auto_ptr 的成員函數(shù)將會(huì)導(dǎo)致不可預(yù)知的后果。所以你必須非常謹(jǐn)慎的使用auto_ptr ,如果他被轉(zhuǎn)移過。

auto_ptr<Shape> make_triangle()

{

    return auto_ptr<Shape>(new Triangle);

}

auto_ptr<Shape> c(make_triangle());      // move temporary into c

double area = make_triangle()->area();   // perfectly safe

auto_ptr<Shape> a(new Triangle);    // create triangle

auto_ptr<Shape> b(a);               // move a into b

double area = a->area();                // undefined behavior

顯然,在持有auto_ptr 對(duì)象的a表達(dá)式和持有調(diào)用函數(shù)返回的auto_ptr值類型的make_triangle()表達(dá)式之間一定有一些潛在的區(qū)別,每調(diào)用一次后者就會(huì)創(chuàng)建一個(gè)新的auto_ptr對(duì)象。這里a 其實(shí)就是一個(gè)左值(lvalue)的例子,而make_triangle()就是右值(rvalue)的例子。

轉(zhuǎn)移像a這樣的左值是非常危險(xiǎn)的,因?yàn)槲覀兛赡苷{(diào)用a的成員函數(shù),這會(huì)導(dǎo)致不可預(yù)知的行為。另一方面,轉(zhuǎn)移像make_triangle()這樣的右值卻是非常安全的,因?yàn)閺?fù)制構(gòu)造函數(shù)之后,我們不能再使用這個(gè)臨時(shí)對(duì)象了,因?yàn)檫@個(gè)轉(zhuǎn)移后的臨時(shí)對(duì)象會(huì)在下一行之前銷毀掉。

我們現(xiàn)在知道轉(zhuǎn)移左值是十分危險(xiǎn)的,但是轉(zhuǎn)移右值卻是很安全的。如果C++能從語言級(jí)別支持區(qū)分左值和右值參數(shù),我就可以完全杜絕對(duì)左值轉(zhuǎn)移,或者把轉(zhuǎn)移左值在調(diào)用的時(shí)候暴露出來,以使我們不會(huì)不經(jīng)意的轉(zhuǎn)移左值。

C++ 11對(duì)這個(gè)問題的答案是右值引用。右值引用是針對(duì)右值的新的引用類型,語法是X&&。以前的老的引用類型X& 現(xiàn)在被稱作左值引用。

使用右值引用X&&作為參數(shù)的最有用的函數(shù)之一就是轉(zhuǎn)移構(gòu)造函數(shù)X::X(X&& source),它的主要作用是把源對(duì)象的本地資源轉(zhuǎn)移給當(dāng)前對(duì)象。

C++ 11中,std::auto_ptr< T >已經(jīng)被std::unique_ptr< T >所取代,后者就是利用的右值引用。

其轉(zhuǎn)移構(gòu)造函數(shù):

unique_ptr(unique_ptr&& source)   // note the rvalue reference

{

    ptr = source.ptr;

    source.ptr = nullptr;

}

這個(gè)轉(zhuǎn)移構(gòu)造函數(shù)跟auto_ptr中復(fù)制構(gòu)造函數(shù)做的事情一樣,但是它卻只能接受右值作為參數(shù)。

unique_ptr<Shape> a(new Triangle);

unique_ptr<Shape> b(a);                 // error

unique_ptr<Shape> c(make_triangle());       // okay

第二行不能編譯通過,因?yàn)?/span>a是左值,但是參數(shù)unique_ptr&& source只能接受右值,這正是我們所需要的,杜絕危險(xiǎn)的隱式轉(zhuǎn)移。第三行編譯沒有問題,因?yàn)?/span>make_triangle()是右值,轉(zhuǎn)移構(gòu)造函數(shù)會(huì)將臨時(shí)對(duì)象的所有權(quán)轉(zhuǎn)移給對(duì)象c,這正是我們需要的。

轉(zhuǎn)移左值

有時(shí)候,我們可能想轉(zhuǎn)移左值,也就是說,有時(shí)候我們想讓編譯器把左值當(dāng)作右值對(duì)待,以便能使用轉(zhuǎn)移構(gòu)造函數(shù),即便這有點(diǎn)不安全。出于這個(gè)目的,C++ 11在標(biāo)準(zhǔn)庫的頭文件< utility >中提供了一個(gè)模板函數(shù)std::move。實(shí)際上,std::move僅僅是簡單地將左值轉(zhuǎn)換為右值,它本身并沒有轉(zhuǎn)移任何東西。它僅僅是讓對(duì)象可以轉(zhuǎn)移。

以下是如何正確的轉(zhuǎn)移左值:

unique_ptr<Shape> a(new Triangle);

unique_ptr<Shape> b(a);              // still an error

unique_ptr<Shape> c(std::move(a));   // okay

請(qǐng)注意,第三行之后,a不再擁有Triangle對(duì)象。不過這沒有關(guān)系,因?yàn)橥ㄟ^明確的寫出std::move(a),我們很清楚我們的意圖:親愛的轉(zhuǎn)移構(gòu)造函數(shù),你可以對(duì)a做任何想要做的事情來初始化c;我不再需要a了,對(duì)于a,您請(qǐng)自便。

當(dāng)然,如果你在使用了mova(a)之后,還繼續(xù)使用a,那無疑是搬起石頭砸自己的腳,還是會(huì)導(dǎo)致嚴(yán)重的運(yùn)行錯(cuò)誤。

總之,std::move(some_lvalue)將左值轉(zhuǎn)換為右值(可以理解為一種類型轉(zhuǎn)換),使接下來的轉(zhuǎn)移成為可能。

一個(gè)例子:

class Foo

{

    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)

    : member(parameter)   // error

    {}

};

上面的parameter,其類型是一個(gè)右值引用,只能說明parameter是指向右值的引用,而parameter本身是個(gè)左值。(Things that are declared as rvalue reference can be lvalues or rvalues. The distinguishing criterion is: if it has a name, then it is an lvalue. Otherwise, it is an rvalue.

因此以上對(duì)parameter的轉(zhuǎn)移是不允許的,需要使用std::move來顯示轉(zhuǎn)換成右值。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多