1. 程式人生 > >C++11常用新特性快速一覽

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

最近工作中,遇到一些問題,使用C++11實現起來會更加方便,而線上的生產環境還不支援C++11,於是決定新年開工後,在組內把C++11推廣開來,整理以下文件,方便自己查閱,也方便同事快速上手。(對於非同步程式設計十分實用的Future/Promise以及智慧指標等,將不做整理介紹,組內使用的框架已經支援並廣泛使用了,用的是自己公司參考boost實現的版本)

1. nullptr

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

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

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

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

void foo(char *);
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 進行型別推導的一個最為常見而且顯著的例子就是迭代器。在以前我們需要這樣來書寫一個迭代器:

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

而有了 auto 之後可以:

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

一些其他的常見用法:

auto i = 5;             // i 被推導為 int
auto arr = new auto(10) // arr 被推導為 int *

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

int add(auto x, auto y);

此外,auto 還不能用於推導陣列型別:

#include <iostream>

int main() {
 auto i = 5;

 int arr[10] = {0};
 auto auto_arr = arr;
 auto auto_arr2[10] = arr;

 return 0;
}

decltype

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

decltype(表示式)

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

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

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

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

template<typename R, typename T, typename U>
R add(T x, U y) {
    return x+y
}

這樣的程式碼其實變得很醜陋,因為程式設計師在使用這個模板函式的時候,必須明確指出返回型別。但事實上我們並不知道 add() 這個函式會做什麼樣的操作,獲得一個什麼樣的返回型別。

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

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

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

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

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

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

3. 區間迭代

基於範圍的 for 迴圈

C++11 引入了基於範圍的迭代寫法,我們擁有了能夠寫出像 Python 一樣簡潔的迴圈語句。
最常用的 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 提供了統一的語法來初始化任意的物件,例如:

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};    // 統一的初始化語法
B b {2, 2.2};

C++11 還把初始化列表的概念繫結到了型別上,並將其稱之為 std::initializer_list,允許建構函式或其他函式像引數一樣使用初始化列表,這就為類物件的初始化與普通陣列和 POD 的初始化方法提供了統一的橋樑,例如:

#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. 模板增強

外部模板

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

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

template class std::vector<bool>;            // 強行例項化
extern template class std::vector<double>;  // 不在該編譯檔案中例項化模板

尖括號 “>”

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

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

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

類型別名模板

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

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 引入了下面這種形式的寫法,並且同時支援對傳統 typedef 相同的功效:

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

預設模板引數

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

template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) {
    return x+y
}

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

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

6. 建構函式

委託構造

C++11 引入了委託構造的概念,這使得建構函式可以在同一個類中一個建構函式呼叫另一個建構函式,從而達到簡化程式碼的目的:

class Base {
public:
    int value1;
    int value2;
    Base() {
        value1 = 1;
    }
    Base(int value) : Base() {  // 委託 Base() 建構函式
        value2 = 2;
    }
};

繼承構造

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

struct A
{
  A(int i) {}
  A(double d,int i){}
  A(float f,int i,const char* c){}
  //...等等系列的建構函式版本
};
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){}
  //......等等好多個和基類建構函式對應的建構函式
};

C++11的繼承構造:

struct A
{
  A(int i) {}
  A(double d,int i){}
  A(float f,int i,const char* c){}
  //...等等系列的建構函式版本
};
struct B:A
{
  using A::A;
  //關於基類各建構函式的繼承一句話搞定
  //......
};

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

7. Lambda 表示式

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

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

[ 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表示式在呼叫時能即時訪問外部變數,我們應當使用引用方式捕獲。

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變數,同時不捕獲其他變數。
6) [this]捕獲當前類中的this指標,讓lambda表示式擁有和當前類成員函式同樣的訪問許可權。如果已經使用了&或者=,就預設新增此選項。捕獲this的目的是可以在lamda中使用當前類的成員函式和成員變數

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是以複製方式捕獲的,無法修改
auto f5 = [a] { return a+b; };                      //error,沒有捕獲變數b
auto f6 = [a, &b] { return a + (b++); };                //OK
auto f7 = [=, &b] { return a + (b++); };                //OK

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

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

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

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

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

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

a = b;   // 非法,lambda無法賦值
auto c = a;   // 合法,生成一個副本

閉包型別禁用了賦值操作符,但是沒有禁用複製建構函式,所以你仍然可以用一個lambda表示式去初始化另外一個lambda表示式而產生副本。

在多種捕獲方式中,最好不要使用[=]和[&]預設捕獲所有變數

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

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

上面函式返回了一個lambda表示式,引數x僅是一個臨時變數,函式add_x呼叫後就被銷燬了,但是返回的lambda表示式卻引用了該變數,當呼叫這個表示式時,引用的是一個垃圾值,會產生沒有意義的結果。上面這種情況,使用預設傳值方式可以避免懸掛引用問題。

但是採用預設值捕獲所有變數仍然有風險,看下面的例子:

class Filter
{
public:
    Filter(int divisorVal):
        divisor{divisorVal}
    {}

    std::function<bool(int)> getFilter() 
    {
        return [=](int value) {return value % divisor == 0; };
    }

private:
    int divisor;
};

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

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

原始碼中,lambda表示式實際上捕捉的是this指標的副本,所以原來的程式碼等價於:

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

儘管還是以值方式捕獲,但是捕獲的是指標,其實相當於以引用的方式捕獲了當前類物件,所以lambda表示式的閉包與一個類物件繫結在一起了,這很危險,因為你仍然有可能在類物件析構後使用這個lambda表示式,那麼類似“懸掛引用”的問題也會產生。所以,採用預設值捕捉所有變數仍然是不安全的,主要是由於指標變數的複製,實際上還是按引用傳值。

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

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

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

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

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; });

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

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; });
// 此時v {1, 1, 2, 3, 5, 8, 13, 21, 34, 55}

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

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表示式來實現想要的效果。

8. 新增容器

std::array

std::array 儲存在棧記憶體中,相比堆記憶體中的 std::vector,我們能夠靈活的訪問這裡面的元素,從而獲得更高的效能。

std::array 會在編譯時建立一個固定大小的陣列,std::array 不能夠被隱式的轉換成指標,使用 std::array只需指定其型別和大小即可:

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

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

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

void foo(int *p, int len) {
    return;
}

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

// C 風格介面傳參
// foo(arr, arr.size());           // 非法, 無法隱式轉換
foo(&arr[0], arr.size());
foo(arr.data(), arr.size());

// 使用 `std::sort`
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: 元組拆包

#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 會出現推斷錯誤, 編譯失敗
}

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;

    // 元組進行拆包
    std::tie(gpa, grade, name) = get_student(1);
    std::cout << "ID: 1, "
    << "GPA: " << gpa << ", "
    << "成績: " << grade << ", "
    << "姓名: " << name << '\n';
}

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

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。例如:

#include <iostream>
#include <string>
#include <regex>

int main() {
    std::string fnames[] = {"foo.txt", "bar.txt", "test", "a0.txt", "AAA.txt"};
    // 在 C++ 中 `\` 會被作為字串內的轉義符,為使 `\.` 作為正則表示式傳遞進去生效,需要對 `\` 進行二次轉義,從而有 `\\.`
    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 三個引數,其中 std::smatch 的本質其實是 std::match_results,在標準庫中, std::smatch 被定義為了 std::match_results,也就是一個子串迭代器型別的 match_results。使用 std::smatch 可以方便的對匹配的結果進行獲取,例如:

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 的第一個元素匹配整個字串
        // sub_match 的第二個元素匹配了第一個括號表示式
        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;
        }
    }
}

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

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. 語言級執行緒支援

std::thread
std::mutex/std::unique_lock
std::future/std::packaged_task
std::condition_variable

程式碼編譯需要使用 -pthread 選項

11. 右值引用和move語義

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

string a(x);                                    // line 1
string b(x + y);                                    // line 2
string c(some_function_returning_a_string());       // line 3

如果使用以下拷貝建構函式:

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

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

第二行和第三行的引數則是右值,因為表示式產生的string物件是匿名物件,之後沒有辦法再使用了。

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

string(string&& that)   // string&& is an rvalue reference to a string
{
data = that.data;
that.data = 0;
}

我們沒有深度拷貝堆記憶體中的資料,而是僅僅複製了指標,並把源物件的指標置空。事實上,我們“偷取”了屬於源物件的記憶體資料。由於源物件是一個右值,不會再被使用,因此客戶並不會覺察到源物件被改變了。在這裡,我們並沒有真正的複製,所以我們把這個建構函式叫做“轉移建構函式”(move constructor),他的工作就是把資源從一個物件轉移到另一個物件,而不是複製他們。

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

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

注意到我們是直接對引數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中已被廢棄,因為其“複製”行為是危險的。

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

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

auto_ptr 的複製建構函式可能看起來像這樣(簡化):

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 的危險之處在於看上去應該是複製,但實際上確是轉移。呼叫被轉移過的auto_ptr 的成員函式將會導致不可預知的後果。所以你必須非常謹慎的使用auto_ptr ,如果他被轉移過。

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 物件的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 >所取代,後者就是利用的右值引用。

其轉移建構函式:

unique_ptr(unique_ptr&& source)   // note the rvalue reference
{
    ptr = source.ptr;
    source.ptr = nullptr;
}

這個轉移建構函式跟auto_ptr中複製建構函式做的事情一樣,但是它卻只能接受右值作為引數。

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

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

轉移左值

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

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

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

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

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

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

一個例子:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};

上面的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來顯示轉換成右值。

相關推薦

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

最近工作中,遇到一些問題,使用C++11實現起來會更加方便,而線上的生產環境還不支援C++11,於是決定新年開工後,在組內把C++11推廣開來,整理以下文件,方便自己查閱,也方便同事快速上手。(對於非同步程式設計十分實用的Future/Promise以及智慧指標

3.1.C++11語言特性

3.1.1微小但重要的語法提升 nullptr和std::nullptr_t : nullptr取代0或NULL,表示一個pointer(指標)只想指向的no value。std::nullptr_t定義在<cstddef>中,為基礎型別。   3.1.2以au

C++面試總結(五)C++ 11/14特性

C++11是自C++98十餘年來發布的一個新特性,擴充了很多C++的功能和特性,而C++14是對C++11的又一次補充和優化,這些新特性使得C++更貼近於一種現代化的變成語言。gcc版本大於5(clang版本大於3.8)已經全面支援C++14。 1.Lambda 表示式 Lambda表示式,

C++11 標準特性: 右值引用與轉移語義

https://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/ 特性的目的 右值引用 (Rvalue Referene) 是 C++ 新標準 (C++11, 11 代表 2011 年 ) 中引入的新特性

C++11 標準特性:Defaulted 和 Deleted 函式

https://www.ibm.com/developerworks/cn/aix/library/1212_lufang_c11new/index.html 本文將介紹 C++11 標準的兩個新特性:defaulted 和 deleted 函式。對於 default

C++14 常用特性總結

1. 返回值型別推導(Return type deduction)為什麼返回型別推導對於C++程式來說是錦上添花的。首先,有時候你必須返回一個非常複雜的型別,比如在對標準庫容器進行搜尋的時候返回一個迭代器。auto返回型別使得函式更加易讀,易寫。其次,這個原因可能不是那麼明顯

C++11/14特性

C++11是自C++98十餘年來發布的一個新特性,擴充了很多C++的功能和特性,而C++14是對C++11的又一次補充和優化,這些新特性使得C++更貼近於一種現代化的變成語言。gcc版本大於5(clang版本大於3.8)已經全面支援C++14,並且在編譯時需要開

C++11特性——std::function

用過C#的人,一般都知道委託和事件。 如果沒有用過C#,我在這裡簡單解釋一下委託,如果用過了,下面可以skip。 委託是一個方法宣告,我們知道,在C語言中,可以通過函式指標表示一個地址的CALL方法,委託在C#中也差不多是幹這樣的工作。 但是委託有一些不同,主要

C++11常用特性的使用經驗總結

概述及目錄(原創部落格,版權所有,轉載請註明出處 http://www.cnblogs.com/feng-sc)   C++11已經出來很久了,網上也早有很多優秀的C++11新特性的總結文章,在編寫本部落格之前,博主在工作和學習中學到的關於C++11方面的知識,也得益於很多

C++11常用特性複習與速記(1)

    C++11標準雖然出來已經很久,但以我一介井底之蛙的學生視角來看,它的推廣在初學者方面做得還蠻不夠的。尤其很多的教材和大學課程,還在使用著舊的特性,對新的更好用的功能涉及地很少。這裡藉助《深入理解C++11》(【加】Michael Wong,IBM XL編譯器中國開發

C++0x語言特性一覽

引入約束的最初動因在於改進編譯錯誤資訊的質量。如果程式設計師試圖使用一種不能提供某個模板所需介面的型別,那麼編譯器將產生錯誤資訊。然而,這些錯誤資訊通常難以理解,尤其對於新手而言。首先,錯誤資訊中的模板引數通常被完整拼寫出來,這將導致異常龐大的錯誤資訊。在某些編譯器上,簡單的錯誤會產生好幾K的錯誤資訊。其次,

C++11功能特性對Boost庫影響

《Boost程式庫探祕——深度解析C++準標準庫》之試讀         前一陣子還看到一篇文章,說C#要重蹈C++的覆轍,這裡說的C++的覆轍是什麼呢?是指C++語言過於臃腫的功能特性,導致學習人員的流失。文章說,語言最後的威力是“開發軟體”,而不是“比拼新特性”    

C#7.0特性

reat href code 轉載 支持 als 有用 sharp object類 轉載自:http://www.cnblogs.com/GuZhenYin/p/6526041.html 微軟昨天發布了新的VS 2017 ..隨之而來的還有很多很多東西... .NET新版

iOS 11 application 特性

reg 分配 hone -s view 退出 cat nis user 1、- (void)applicationWillResignActive:(UIApplication *)application 說明:當應用程序將要入非活動狀態執行,在此期間,應用程序不接收消息或

C# 7.0 特性:本地方法

性能 erro 區別 visual html 修飾 之間 style ria C# 7.0:本地方法 VS 2017 的 C# 7.0 中引入了本地方法,本地方法是一種語法糖,允許我們在方法內定義本地方法。更加類似於函數式語言,但是,本質上還是基於面向對象實現的。 1.

詳解C#7.0特性

numeric base rdquo 字母 and throw cal odin png 1. out 變量(out variables) 以前我們使用out變量必須在使用前進行聲明,C# 7.0 給我們提供了一種更簡潔的語法 “使用時進行內聯聲明&r

c++11標準

函數返回 ima 銷毀 socket comment 模板類 創建 包裝 c++98 數相同作用域的變量參考也可以被使用。這種的變量集合一般被稱作 closure(閉包)。我在這裏就不再講這個事了。表達式的簡單語法如下, 1 [capture](param

C#4.0特性之協變與逆變實例分析

alt out thumb def 3.0 介紹 ted 路徑 運行 本文實例講述了C#4.0新特性的協變與逆變,有助於大家進一步掌握C#4.0程序設計。具體分析如下: 一、C#3.0以前的協變與逆變 如果你是第一次聽說這個兩個詞,別擔心,他們其實很常見。C#4.0中

Java8常用特性實踐

arr 明顯 tro 設計 特性 nbsp imp image show 前言:   時下Oracle開速叠代的Java社區以即將推出Java10,但尷尬的是不少小中企業仍使用JDK7甚至JDK6開發。   從上面列出的JDK8特性中我們可以發現Java8的部分特性很

轉載:C#7.0特性(VS2017可用)

AD mage 運行 str translate adc 微軟 returns false 前言 微軟昨天發布了新的VS 2017 ..隨之而來的還有很多很多東西... .NET新版本 ASP.NET新版本...等等..太多..實在沒消化.. 分享一下其實201