c++ STL(七 c++11新特性速覽)


1.nullptr

nullptr 出現的目的是為了替代 NULL。

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

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

而這依然會產生問題,將導致了 C++ 中重載特性會發生混亂,考慮:

1 void foo(char *);
2 void foo(int);

對於這兩個函數來說,如果 NULL 又被定義為了 0 那么 foo(NULL); 這個語句將會去調用 foo(int),從而導致代碼違反直觀。

為了解決這個問題,C++11 引入了 nullptr 關鍵字,專門用來區分空指針、0。

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

當需要使用 NULL 時候,養成直接使用 nullptr的習慣。

2.類型推導

C++11 引入了 auto 和 decltype 這兩個關鍵字實現了類型推導,讓編譯器來操心變量的類型。

auto
auto 在很早以前就已經進入了 C++,但是他始終作為一個存儲類型的指示符存在,與 register 並存。在傳統 C++ 中,如果一個變量沒有聲明為 register 變量,將自動被視為一個 auto 變量。而隨着 register 被棄用,對 auto 的語義變更也就非常自然了。

使用 auto 進行類型推導的一個最為常見而且顯著的例子就是迭代器。在以前我們需要這樣來書寫一個迭代器:
 1 for(vector<int>::const_iterator itr = vec.cbegin(); itr != vec.cend(); ++itr) 

而有了 auto 之后可以:

1 //由於cbegin()將返回vector<int>::const_iterator
2 //所以itr也應該是vector<int>::const_iterator類型
3 for(auto itr = vec.cnegin(); itr != vec.cend(); ++itr);

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

 1 int add(auto x, auto y); 

此外,auto 還不能用於推導數組類型

復制代碼
 1 #include <iostream>
 2 
 3 int main() {
 4  auto i = 5;
 5 
 6  int arr[10] = {0};
 7  auto auto_arr = arr;  //錯誤
 8  auto auto_arr2[10] = arr;  //錯誤
 9 
10  return 0;
11 }
復制代碼

decltype

decltype 關鍵字是為了解決 auto 關鍵字只能對變量進行類型推導的缺陷而出現的。它的用法和 sizeof 很相似:

 1 decltype(表達式) 

在此過程中,編譯器分析表達式並得到它的類型,卻不實際計算表達式的值。 
有時候,我們可能需要計算某個表達式的類型,例如:

1 auto x=1;
2 auto y=2;
3 decltype(x+y) z;

拖尾返回類型、auto 與 decltype 配合

你可能會思考,auto 能不能用於推導函數的返回類型。考慮這樣一個例子加法函數的例子,在傳統 C++ 中我們必須這么寫

1 template<typename R,typename T,typename U>
2 R add(T x,U y){
3     return x+y;
4 }

這樣的代碼其實變得很丑陋,因為程序員在使用這個模板函數的時候,必須明確指出返回類型。但事實上我們並不知道 add() 這個函數會做什么樣的操作,獲得一個什么樣的返回類型。

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

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

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

1 template<typename T, typename U>
2 auto add(T x, U y)->decltype(x+y){
3     return x+y;
4 }

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

1 template<typename T, typename U>
2 auto add(T x, U y){
3     return x+y;
4 }

3. 區間迭代

基於范圍的 for 循環

C++11 引入了基於范圍的迭代寫法,我們擁有了能夠寫出像 Python 一樣簡潔的循環語句。 
最常用的 std::vector 遍歷將從原來的樣子:

1 std::vector<int> arr(5,100);
2 for(std::vector<int>::iterator i=arr.begin();i!=arr.end();++i){
3     std::cout<<*i<<std::endl;
4 }

變得非常簡單:

1 //&啟用了引用
2 for(auto &i:arr){
3     std::cout<<i<<std::endl;
4 }

4. 初始化列表

C++11 提供了統一的語法來初始化任意的對象,例如:

復制代碼
 1 struct A{
 2     int a;
 3     float b;
 4 };
 5 struct B{
 6     B(int _a,float _b):a(_a),b(_b){}
 7 private:
 8     int a;
 9     float b;
10 };
11 
12 A a{1,1.1};  //統一初始化語法
13 B b{2,2.2};
復制代碼

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

復制代碼
1 #include<initializer_list>
2 class Magic{
3 public:
4     Magic(std::initializer_list<int> list){}
5 };
6 Magic magic={1,2,3,4,5};
7 std::vector<int>v={1,2,3,4};
復制代碼

5. 模板增強

外部模板

傳統 C++ 中,模板只有在使用時才會被編譯器實例化。只要在每個編譯單元(文件)中編譯的代碼中遇到了被完整定義的模板,都會實例化。這就產生了重復實例化而導致的編譯時間的增加。並且,我們沒有辦法通知編譯器不要觸發模板實例化。

C++11 引入了外部模板,擴充了原來的強制編譯器在特定位置實例化模板的語法,使得能夠顯式的告訴編譯器何時進行模板的實例化:

1 template class std::vector<bool>;        //強行實例化
2 extren template class std::vector<double>;//不在編譯文件中實例化模板

尖括號 “>”

在傳統 C++ 的編譯器中,>>一律被當做右移運算符來進行處理。但實際上我們很容易就寫出了嵌套模板的代碼:

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

這在傳統C++編譯器下是不能夠被編譯的,而 C++11 開始,連續的右尖括號將變得合法,並且能夠順利通過編譯。

類型別名模板

在傳統 C++中,typedef 可以為類型定義一個新的名稱,但是卻沒有辦法為模板定義一個新的名稱。因為,模板不是類型。例如:

復制代碼
1 template< typename T, typename U, int value>
2 class SuckType {
3 public:
4     T a;
5     U b;
6     SuckType():a(value),b(value){}
7 };
8 template< typename U>
9 typedef SuckType<std::vector<int>, U, 1> NewType; // 不合法
復制代碼

C++11 使用 using 引入了下面這種形式的寫法,並且同時支持對傳統 typedef 相同的功效:

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

默認模板參數

我們可能定義了一個加法函數:

1 template<typename T,typename U>
2 auto add(T x,U y)->decltype(x+y){
3     return x+y;
4 }

但在使用時發現,要使用 add,就必須每次都指定其模板參數的類型。 
在 C++11 中提供了一種便利,可以指定模板的默認參數

1 template<typename T=int,typename U=int>
2 auto add(T x,U y)->decltype(x+y){
3     return x+y;
4 }

6. 構造函數

委托構造

C++11 引入了委托構造的概念,這使得構造函數可以在同一個類中一個構造函數調用另一個構造函數,從而達到簡化代碼的目的:

復制代碼
 1 class Base{
 2 public:
 3     int value1;
 4     int value2;
 5     Base(){
 6         value1=1;
 7     }
 8     Base(int value):Base(){//委托Base()構造函數
 9         value2=2;
10     }
11 }
復制代碼

繼承構造

在繼承體系中,如果派生類想要使用基類的構造函數,需要在構造函數中顯式聲明。 
假若基類擁有為數眾多的不同版本的構造函數,這樣,在派生類中得寫很多對應的“透傳”構造函數。如下:

復制代碼
 1 struct A{
 2     A(int i){}
 3     A(double d,int i){}
 4     A(float f,int i,const char* c){}
 5     //....等等系列的構造函數版本
 6 };
 7 struct B:A{
 8     B(int i):A(i){}
 9     B(double d,int i):A(d,i){}
10     B(float f,int i,const char* c):A(f,i,e){}
11     //....等等好多個和基類構造函數對應的構造函數
12 };
復制代碼

C++11的繼承構造:

復制代碼
 1 struct A{
 2     A(int i){}
 3     A(double d,int i){}
 4     A(float f,int i,const char* c){}
 5     //....等等系列的構造函數版本
 6 };
 7 struct B:A{
 8     using A::A;
 9     //關於基類各構造函數的繼承一句話搞定
10     //.....
11 };
復制代碼

如果一個繼承構造函數不被相關的代碼使用,編譯器不會為之產生真正的函數代碼,這樣比透傳基類各種構造函數更加節省目標代碼空間。

7. Lambda 表達式

 Lambda表達式,實際上就是提供了一個類似匿名函數的特性,而匿名函數則是在需要一個函數,但是又不想費力去命名一個函數的情況下去使用的。

Lambda表達式的基本語法如下:

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

1) capture是捕獲列表; 
2) params是參數表;(選填) 
3) opt是函數選項;可以填mutable,exception,attribute(選填) 
mutable說明lambda表達式體內的代碼可以修改被捕獲的變量,並且可以訪問被捕獲的對象的non-const方法。 
exception說明lambda表達式是否拋出異常以及何種異常。 
attribute用來聲明屬性。 
4) ret是返回值類型(拖尾返回類型)。(選填) 
5) body是函數體。

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

1) []不捕獲任何變量。 
2) [&]捕獲外部作用域中所有變量,並作為引用在函數體中使用(按引用捕獲)。 
3) [=]捕獲外部作用域中所有變量,並作為副本在函數體中使用(按值捕獲)。注意值捕獲的前提是變量可以拷貝,且被捕獲的變量在 lambda 表達式被創建時拷貝,而非調用時才拷貝。如果希望lambda表達式在調用時能即時訪問外部變量,我們應當使用引用方式捕獲。
按值捕獲

1 int a=0;
2 auto f=[=]{return a;};
3 a+=1;
4 cout<<f()<<endl;    //輸出0

按引用捕獲

1 int a=0;
2 auto f=[&a]{return a;};
3 a+=1;
4 cout<<f()<<endl;    //輸出1

4) [=,&foo]按值捕獲外部作用域中所有變量,並按引用捕獲foo變量。 
5) [bar]按值捕獲bar變量,同時不捕獲其他變量。 
6) [this]捕獲當前類中的this指針,讓lambda表達式擁有和當前類成員函數同樣的訪問權限。如果已經使用了&或者=,就默認添加此選項。捕獲this的目的是可以在lamda中使用當前類的成員函數和成員變量。

復制代碼
 1 class A{
 2 public:
 3     int i_=0;
 4     void func(int x,int y){
 5         auto x1=[]{return i_;};    //error,沒有捕獲外部變量
 6         auto x2=[=]{return i_+x+y;};    //OK
 7         auto x3=[&]{return i_+x+y;};        //OK
 8         auto x4=[this]{return i_;};        //OK
 9         auto x5=[this]{return i_+x+y;};    //error,沒有捕獲x,y
10         auto x6=[this,x,y]{return i_+x+y;};    //OK
11         auto x7=[this]{return i_++;};        //OK
12     }
13 }
14 
15 int a=0,b=1;
16 auto f1=[]{return a;};        //error,沒有捕獲外部變量
17 auto f2=[&]{return a++;};    //OK
18 auto f3=[=]{return a;};        //OK
19 auto f4=[=]{return a++;};    //error,a是以復制方式捕獲的,無法修改
20 auto f5=[a]{return a+b;};    //error,沒有捕獲變量b
21 auto f6[a,&b]{return a+(b++);};    //OK
22 auto f7=[=,&b]{return a+(b++);};    //OK
復制代碼

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

原因:lambda表達式可以說是就地定義仿函數閉包的“語法糖”。它的捕獲列表捕獲住的任何外部變量,最終會變為閉包類型的成員變量。按照C++標准,lambda表達式的operator()默認是const的,一個const成員函數是無法修改成員變量的值的。而mutable的作用,就在於取消operator()的const。

1 int a=0;
2 auto f1=[=]{return a++;};    //error
3 auto f2=[=]()mutable{return a++;};    //OK

lambda表達式的大致原理:每當你定義一個lambda表達式后,編譯器會自動生成一個匿名類(這個類重載了()運算符),我們稱為閉包類型(closure type)。那么在運行時,這個lambda表達式就會返回一個匿名的閉包實例,是一個右值。所以,我們上面的lambda表達式的結果就是一個個閉包。對於復制傳值捕捉方式,類中會相應添加對應類型的非靜態數據成員。在運行時,會用復制的值初始化這些成員變量,從而生成閉包。對於引用捕獲方式,無論是否標記mutable,都可以在lambda表達式中修改捕獲的值。至於閉包類中是否有對應成員,C++標准中給出的答案是:不清楚的,與具體實現有關。

lambda表達式是不能被賦值的:

1 auto a=[]{cout<<"A"<<endl;};
2 auto b=[]{cout<<"B"<<endl;};
3 a=b;    //非法,lambda無法賦值
4 auto c=a;    //合法,生成一個副本

閉包類型禁用了賦值操作符,但是沒有禁用復制構造函數,所以你仍然可以用一個lambda表達式去初始化另外一個lambda表達式而產生副本。

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

默認引用捕獲所有變量,你有很大可能會出現懸掛引用(Dangling references),因為引用捕獲不會延長引用的變量的生命周期:

1 std::function<int(int)> add_x(int x){
2     return [&](int a){return x+a;};
3 }

上面函數返回了一個lambda表達式,參數x僅是一個臨時變量,函數add_x調用后就被銷毀了,但是返回的lambda表達式卻引用了該變量,當調用這個表達式時,引用的是一個垃圾值,會產生沒有意義的結果。上面這種情況,使用默認傳值方式可以避免懸掛引用問題。

但是采用默認值捕獲所有變量仍然有風險,看下面的例子:

復制代碼
 1 class Filter
 2 {
 3 public:
 4     Filter(int divisorVal):
 5         divisor{divisorVal}
 6     {}
 7 
 8     std::function<bool(int)> getFilter() 
 9     {
10         return [=](int value) {return value % divisor == 0; };
11     }
12 
13 private:
14     int divisor;
15 };
復制代碼

這個類中有一個成員方法,可以返回一個lambda表達式,這個表達式使用了類的數據成員divisor。而且采用默認值方式捕捉所有變量。你可能認為這個lambda表達式也捕捉了divisor的一份副本,但是實際上並沒有。因為數據成員divisor對lambda表達式並不可見,你可以用下面的代碼驗證:

1 // 類的方法,下面無法編譯,因為divisor並不在lambda捕捉的范圍
2 std::function<bool(int)> getFilter() 
3 {
4     return [divisor](int value) {return value % divisor == 0; };
5 }

原代碼中,lambda表達式實際上捕捉的是this指針的副本,所以原來的代碼等價於:

1 std::function<bool(int)> getFilter() 
2 {
3     return [this](int value) {return value % this->divisor == 0; };
4 }

盡管還是以值方式捕獲,但是捕獲的是指針,其實相當於以引用的方式捕獲了當前類對象,所以lambda表達式的閉包與一個類對象綁定在一起了,這很危險,因為你仍然有可能在類對象析構后使用這個lambda表達式,那么類似“懸掛引用”的問題也會產生。所以,采用默認值捕捉所有變量仍然是不安全的,主要是由於指針變量的復制,實際上還是按引用傳值。

lambda表達式可以賦值給對應類型的函數指針。但是使用函數指針並不是那么方便。所以STL定義在< functional >頭文件提供了一個多態的函數對象封裝std::function,其類似於函數指針。它可以綁定任何類函數對象,只要參數與返回類型相同。如下面的返回一個bool且接收兩個int的函數包裝器:

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

lambda表達式一個更重要的應用是其可以用於函數的參數,通過這種方式可以實現回調函數。

最常用的是在STL算法中,比如你要統計一個數組中滿足特定條件的元素數量,通過lambda表達式給出條件,傳遞給count_if函數:

1 int value=3;
2 vector<int> v{1,3,5,2,6,10};
3 int count=std::count_if(v.begin(),v.end(),[value](int x){return x>value;});

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

1 vector<int> v(10);
2 int a=0;
3 int b=1;
4 std::generate(v.begin(),v.end(),[&a,&b]{int value=b;b=b+a;a=value;return value;});
5 //此時v{1,1,2,3,5,8,13,21,34,55}

當需要遍歷容器堆每個元素進行操作時:

復制代碼
1 std::vector<int> v={1,2,3,4,5,6};
2 int even_count=0;
3 for_each(v.begin(),v.end(),[&even_count](int val){
4     if(!(val&1)){
5         ++even_count;
6     }
7 });
8 std::cout<<"The number of even is"<<even_count<<std::endl;
復制代碼

大部分STL算法,可以非常靈活地搭配lambda表達式來實現想要的效果。

8. 新增容器

std::array

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

std::array 會在編譯時創建一個固定大小的數組,std::array 不能夠被隱式的轉換成指針,使用 std::array只需指定其類型和大小即可:

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

當我們開始用上了std::array時,難免會遇到要將其兼容C風格的接口,這里有三種做法:

復制代碼
 1 void foo(int *p,int len){
 2     return ;
 3 }
 4 std::array<int 4> arr={1,2,3,4};
 5 //C風格接口傳參
 6 //foo(arr,arr.size());    //非法,無法隱式轉換
 7 foo(&arr[0],arr.size());
 8 foo(arr.data(),arr.size());
 9 
10 //使用'std::sort'
11 std::sort(arr.begin(),arr.end());
復制代碼

std::forward_list

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

和std::list的雙向鏈表的實現不同,std::forward_list使用單向鏈表進行實現,提供了O(1)復雜度的元素插入,不支持快速隨機訪問(這也是鏈表的特點),也是標准庫容器中唯一一個不提供size()方法的容器。當不需要雙向迭代時,具有比std::list更高的空間利用率。

無序容器

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

std::unordered_map/std::unordered_multimap和

std::unordered_set/std::unordered_multiset。

無序容器中的元素是不進行排序的,內部通過 Hash 表實現,插入和搜索元素的平均復雜度為 O(constant)。

元組 std::tuple

元組的使用有三個核心的函數:

std::make_tuple:構造元組

std::get:獲得元組某個位置的值

std::tie:元組

復制代碼
 1 #include<tuple>
 2 #include<iostream>
 3 auto get_student(int id){
 4     //返回類型被推斷為std::tuple<double,char,std::string>
 5     if(id==0)
 6         return std::make_tuple(3.8,'A',"張三");
 7     if(id==1)
 8         return std::make_tuple(2.9,'C',"李四");
 9     return std::make_tuple(0.0,'D',"null");
10     //如果只寫0會出現推斷錯誤,編譯失敗
11 }
12 int main(){
13     auto student=get_student(0);
14     std::cout<<"ID:0"
15              <<"GPA:"<<std::get<0>(student)<<","
16              <<"成績:"<<std::get<2>(student)<<'\n';
17     double gpa;
18     char grade;
19     std::string  name;
20 
21     //元組進行拆包
22     std::tie(gpa,grade,name)=get_student(1);
23     std::cout<<"ID:1"
24              <<"GPA:"<<gpa<<","
25              <<"成績:"<<grade<<","
26              <<"姓名:"<<name<<'\n';
27     return 0;
28 }
復制代碼

合並兩個元組,可以通過 std::tuple_cat 來實現。

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

9. 正則表達式

正則表達式描述了一種字符串匹配的模式。一般使用正則表達式主要是實現下面三個需求: 
1) 檢查一個串是否包含某種形式的子串; 
2) 將匹配的子串替換; 
3) 從某個串中取出符合條件的子串。

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

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

[a-z]+.txt: 在這個正則表達式中, [a-z] 表示匹配一個小寫字母, + 可以使前面的表達式匹配多次,因此 [a-z]+ 能夠匹配一個及以上小寫字母組成的字符串。在正則表達式中一個 . 表示匹配任意字符,而 . 轉義后則表示匹配字符 . ,最后的 txt 表示嚴格匹配 txt 這三個字母。因此這個正則表達式的所要匹配的內容就是文件名為純小寫字母的文本文件。 
std::regex_match 用於匹配字符串和正則表達式,有很多不同的重載形式。最簡單的一個形式就是傳入std::string 以及一個 std::regex 進行匹配,當匹配成功時,會返回 true,否則返回 false。例如:

復制代碼
 1 #include<iotream>
 2 #include<string>
 3 #include<regex>
 4 int main(){
 5     std::string  fnames[]={"foo.txt","bar.txt","test","a0.txt","AAA.txt"};
 6     //在C++中'\'會被作為字符串內的轉椅符,為使'\.'作為正則表達式傳遞進去生效,需要對'\'進行轉義
 7     std::regex txt_regex("[a-z]+\\.txt");
 8     for (const auto &fname:fames)
 9         std::cout<<fname<<":"<<std::regex_match(fname,txt_regex)<<std::endl;
10     return 0;
11 }
復制代碼

另一種常用的形式就是依次傳入 std::string/std::smatch/std::regex 三個參數,其中 std::smatch 的本質其實是 std::match_results,在標准庫中, std::smatch 被定義為了 std::match_results,也就是一個子串迭代器類型的 match_results。使用 std::smatch 可以方便的對匹配的結果進行獲取,例如:

復制代碼
 1 std::regex base_regex("[a-z]+\\.txt");
 2 std::smatch base_match;
 3 for(const auto&fname:fnames){
 4     if(std::regex_match(fname,base_match,base_regex)){
 5         //sub_match的第一個元素匹配整個字符串
 6         //sub_match的第二個元素匹配了第一個括號表達式
 7         if(base_match.size()==1){
 8             std::string base = base_match[1].str();
 9             std::cout<<"sub_match[0]:"<<base_match[0].str()<<std::endl;
10             std::cout<<fname<<"sub_match[1]:"<<base<<std::endl;
11         }
12     }
13 }
復制代碼

   以上兩個代碼段的輸出結果為:

復制代碼
foo.txt: 1
bae.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. 語言級線程支持

std::thread

std::mutex/std::unique_lock

std::futrue/std::packaged_task

std::condition_variable

代碼編譯需要使用-pthread選線

11. 右值引用和move語義

先看一個簡單的例子直觀感受一下:

1 string a(x);         //line1
2 string b(+y);        //line2 
3 string c(some_function_returning_a_string());   //line3

 

如果使用以下拷貝構造函數:

1 string (const string& that){
2     size_t size=strlen(that.data)+1;
3     data=new char[size];
4     memcpy(data.that.data,size);
5 }

以上3行中,只有第一行(line 1)的x深度拷貝是有必要的,因為我們可能會在后邊用到x,x是一個左值(lvalues)。

第二行和第三行的參數則是右值,因為表達式產生的string對象是匿名對象,之后沒有辦法再使用了。

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

1 string(string&& that){
2     data=that.data;
3     that.data=0;
4 }

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

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

1 string& operator=(string that){
2     std::swap(data,that.data);
3     return *this;
4 }

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

如果是a=b,這樣就會調用復制構造函數來初始化that(因為b是左值),賦值操作符會與新創建的對象交換數據,深度拷貝。這就是copy and swap 慣用法的定義:構造一個副本,與副本交換數據,並讓副本在作用域內自動銷毀。這里也一樣。

如果是a = x + y,這樣就會調用轉移構造函數來初始化that(因為x+y是右值),所以這里沒有深度拷貝,只有高效的數據轉移。相對於參數,that依然是一個獨立的對象,但是他的構造函數是無用的(trivial),因此堆中的數據沒有必要復制,而僅僅是轉移。沒有必要復制他,因為x+y是右值,再次,從右值指向的對象中轉移是沒有問題的。

總結一下:復制構造函數執行的是深度拷貝,因為源對象本身必須不能被改變。而轉移構造函數卻可以復制指針,把源對象的指針置空,這種形式下,這是安全的,因為用戶不可能再使用這個對象了。

下面我們進一步討論右值引用和move語義。

C++98標准庫中提供了一種唯一擁有性的智能指針std::auto_ptr,該類型在C++11中已被廢棄,因為其“復制”行為是危險的。

1 auto_ptr<Shape>a(new Triangle);
2 auto_ptr<Shape>b(a);

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

auto_ptr 的復制構造函數可能看起來像這樣(簡化):

1 auto_ptr(auto_ptr& source){   //note the missing const 
2     p=source.p;
3     source.p=0;   //now the source no longer owns the object
4 }

auto_ptr 的危險之處在於看上去應該是復制,但實際上確是轉移。調用被轉移過的auto_ptr 的成員函數將會導致不可預知的后果。所以你必須非常謹慎的使用auto_ptr ,如果他被轉移過。

復制代碼
 1 auto_ptr<Shape> make_triangle()
 2 {
 3     return auto_ptr<Shape>(new Triangle);
 4 }
 5 
 6 auto_ptr<Shape> c(make_triangle());      // move temporary into c
 7 double area = make_triangle()->area();   // perfectly safe
 8 
 9 auto_ptr<Shape> a(new Triangle);    // create triangle
10 auto_ptr<Shape> b(a);               // move a into b
11 double area = a->area();                // undefined behavior
復制代碼

顯然,在持有auto_ptr 對象的a表達式和持有調用函數返回的auto_ptr值類型的make_triangle()表達式之間一定有一些潛在的區別,每調用一次后者就會創建一個新的auto_ptr對象。這里a 其實就是一個左值(lvalue)的例子,而make_triangle()就是右值(rvalue)的例子。

轉移像a這樣的左值是非常危險的,因為我們可能調用a的成員函數,這會導致不可預知的行為。另一方面,轉移像make_triangle()這樣的右值卻是非常安全的,因為復制構造函數之后,我們不能再使用這個臨時對象了,因為這個轉移后的臨時對象會在下一行之前銷毀掉。

我們現在知道轉移左值是十分危險的,但是轉移右值卻是很安全的。如果C++能從語言級別支持區分左值和右值參數,我就可以完全杜絕對左值轉移,或者把轉移左值在調用的時候暴露出來,以使我們不會不經意的轉移左值。

C++ 11對這個問題的答案是右值引用。右值引用是針對右值的新的引用類型,語法是X&&。以前的老的引用類型X& 現在被稱作左值引用。

使用右值引用X&&作為參數的最有用的函數之一就是轉移構造函數X::X(X&& source),它的主要作用是把源對象的本地資源轉移給當前對象。

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

其轉移構造函數:

1 unique_ptr(unique_ptr&& source)   // note the rvalue reference
2 {
3     ptr = source.ptr;
4     source.ptr = nullptr;
5 }

這個轉移構造函數跟auto_ptr中復制構造函數做的事情一樣,但是它卻只能接受右值作為參數。

1 unique_ptr<Shape> a(new Triangle);
2 unique_ptr<Shape> b(a);                 // error
3 unique_ptr<Shape> c(make_triangle());       // okay

第二行不能編譯通過,因為a是左值,但是參數unique_ptr&& source只能接受右值,這正是我們所需要的,杜絕危險的隱式轉移。第三行編譯沒有問題,因為make_triangle()是右值,轉移構造函數會將臨時對象的所有權轉移給對象c,這正是我們需要的。

轉移左值

有時候,我們可能想轉移左值,也就是說,有時候我們想讓編譯器把左值當作右值對待,以便能使用轉移構造函數,即便這有點不安全。出於這個目的,C++ 11在標准庫的頭文件< utility >中提供了一個模板函數std::move。實際上,std::move僅僅是簡單地將左值轉換為右值,它本身並沒有轉移任何東西。它僅僅是讓對象可以轉移

以下是如何正確的轉移左值:

1 unique_ptr<Shape> a(new Triangle);
2 unique_ptr<Shape> b(a);              // still an error
3 unique_ptr<Shape> c(std::move(a));   // okay

請注意,第三行之后,a不再擁有Triangle對象。不過這沒有關系,因為通過明確的寫出std::move(a),我們很清楚我們的意圖:親愛的轉移構造函數,你可以對a做任何想要做的事情來初始化c;我不再需要a了,對於a,您請自便。

當然,如果你在使用了mova(a)之后,還繼續使用a,那無疑是搬起石頭砸自己的腳,還是會導致嚴重的運行錯誤。

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

一個例子:

復制代碼
 1 class Foo
 2 {
 3     unique_ptr<Shape> member;
 4 
 5 public:
 6 
 7     Foo(unique_ptr<Shape>&& parameter)
 8     : member(parameter)   // error
 9     {}
10 };
復制代碼

上面的parameter,其類型是一個右值引用,只能說明parameter是指向右值的引用,而parameter本身是個左值。(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.)

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM