C++11新特性(一):語言特性

LemHou發表於2024-08-24

C++11新特性

總結C++11特性時發現整個內容較多,建議檢視前先檢視目錄。

語言特性

右值引用

右值的分類為將亡值和字面量。將亡值就是將要銷燬的物件以及臨時的變數,字面量就是常量。左值就是變數。
右值引用,通常使用&&表示。

// 字面量
int a = 1; // a變數,左值;b變數,右值
int Func(int a, int b){
    return a + b;
}
int res = Func(1, 2); // 此時Func(1, 2)返回的就是一個臨時變數,即是右值。res變數是一個左值,\
延長了Func(1, 2)右值的生命週期。
int& b = a; // 左值引用
int&& c = a; // 右值引用

移動語義

移動語義指的是將一個物件的資源所有權轉發給另一個物件。常用於類的移動建構函式和移動賦值運算子。
只要類中有任何建構函式,就不會生成預設建構函式

int a = 1;
int res = Func(1, 2);
int&& b = move(a); // move用來實現左值到右值轉變 
int&& c = move(a); // std::move


class MyClass{
public:
    MyClass() = default; // 預設建構函式
    MyClass(MyClass& obj){ // 複製建構函式
        cout << "left value" << endl;
    }
    MyClass(MyClass&& obj){ // 移動建構函式
        cout << "right value" << endl;
    }
    ~MyClass(){}
};
int main()
{
    MyClass a;
    MyClass b(a); // left value
    MyClass&& xs = move(a); // 右值
    MyClass c(xs); // left value,此時為輸出left value的原因是因為本身右值引用也是一個變數,\
    所以在呼叫建構函式時,會呼叫複製建構函式
    MyClass d(move(xs)); // 使用move直接轉變為右值才會呼叫移動建構函式
}

通常複製建構函式實現的是物件的淺複製,淺複製指的是將兩個物件的指標指向同一塊資源。深複製指的是直接複製一份資源給新的物件。

淺複製的缺點是當舊物件析構時,會將資源一起釋放掉,而新物件指標此時還指向被釋放掉的資源,那麼新物件進行釋放的時候,就會發生double free這種重複釋放問題。

移動語義的好處是,既不用像深複製一樣複製一份資源而是將新物件指標指向舊物件指向的資源,然後將舊指標置為空即可。

移動語義實現:

class MyClass{
private:
    int* data
public:
    MyClass() = default; // 預設建構函式
#if 0
    MyClass(MyClass& obj){ // 複製建構函式,淺複製
        cout << "left value" << endl;
        data = obj.data; // 只複製指標的值
    }
#else
    MyClass(MyClass& obj){ // 複製建構函式,深複製
        cout << "left value" << endl;
        data = new int(*obj.data); // 分配記憶體、賦值為就物件的資源並返回一個指標給data
    }
#endif
    MyClass(MyClass&& obj){ // 移動建構函式,移動語義實現
        this.data = obj.data;
        obj.data = nullptr;
    }
    ~MyClass(){}
};

轉發引用

轉發引用的實現由T&&或者auto&&實現,T是一個模板引數。這能夠用於實現完美轉發,即傳遞引數的同時還能保證其值的型別(左值就是左值,暫時物件、變數傳遞作為右值)。如下程式碼,如果我們直接將右值傳到建構函式中,那麼建構函式會認為此右值變數是一個左值,從而呼叫複製建構函式,並不會呼叫移動建構函式。

class MyClass{
public:
    MyClass() = default; // 預設建構函式
    MyClass(MyClass& obj){ // 複製建構函式
        cout << "left value" << endl;
    }
    MyClass(MyClass&& obj){ // 移動建構函式
        cout << "right value" << endl;
    }
    ~MyClass(){}
};
int main()
{
    MyClass a;
    MyClass b(a); // left value
    MyClass&& xs = move(a); // 右值
    MyClass c(xs); // left value,此時為輸出left value的原因是因為本身右值引用也是一個變數,\
    所以在呼叫建構函式時,會呼叫複製建構函式
    MyClass d(move(xs)); // 此時必須使用move直接轉變為右值才能呼叫移動建構函式
}

當我們引入T&&就可以解決這個問題,此時會直接根據引數的型別自動推導型別。

  • T& &-->T&
  • T& &&-->T&
  • T&& &-->T&
  • T&& &&-->T&&
int a = 0; // int : 左值
auto&& b = a; // int& : 左值
auto&& c = 0; // int&& :右值

接下來我們可以借用T&&auto&&來實現完美轉發。注意完美轉發解決的問題是引數傳遞時型別會發生轉換的問題。

template<typename T>
void Func(T&){
    cout << "left value" << endl;
}

template<typename T>
void Func(T&&){
    cout << "right value" << endl;
}
int a = 0;
auto&& b = a;
auto&& c = 0;
Func(1); // 1 call Func(int&&)
Func(a); // a call Func(int&)
Func(c); // c vall Func(int&), 右值引用直接作為傳遞會發生退化,退化為左值,因為右值本身就是一個變數
Func(move(a)); // call Func(int&&)



// 完美轉發:使用std::forward<T>(arg)實現
template<typename T>
void makeResource(T&& arg) {
  Func(forward<T>(arg));
}
makeResource<int>(c); // 此時傳遞右值引用到引數,forward就會保證引數的型別不變進行傳遞,從而呼叫Func(T&&)

可變引數模板

...語義建立一個引數包或者擴充套件引數包,模板引數包接受0或多個模板引數。有至少一個引數包的模板叫做可變引數模板。

template<typename... T>
void Func(){
    cout << "nihao";
}

#include <numeric>
template<typename T1, typename... Args>
auto Sum(T1 first, Args... args) -> decltype(first)
{
    auto values = {first, args...}; // ...擴充套件引數包
    return accumulate(values.begin(), values.end(), first);
}
cout << Sum(1, 22, 33, 66) << endl;

列表初始化

列表初始化,就是使用花括號進行初始化。{1, 2, 3}會建立一個整數序列,有著型別initial_list<int>
cpp std::vector<int> nums{1, 2, 3, 4, 5}; // 列表初始化 for (auto it : nums){ cout << it << endl; }

靜態斷言

編譯時斷言機制,允許在編譯時檢查某個條件是否為真。靜態編譯檢查編譯時的常量表示式。經常與型別特徵type traits一起使用。因為靜態斷言是在編譯時進行檢驗,所以可以用於不同編譯平臺進行執行前檢驗。

static_assert(<constant-expressions>, <error-message>);
static_assert(sizeof(int) >= 4, "Not support 32 bits integer");
template <typename T>
void CheckInt(T* co, size_t size)
{
    static_assert(std::is_integral<T>::value, "Not support 32 bits integer.");
}

型別推導

auto型別推導的使用,必須前面有可以隱式說明型別的語句。
cpp std::vector<int> nums; for (auto it : nums){ cout << it << endl; }

lambda表示式

形如[capture list](parameter){/* code */},預設捕獲到的值編譯器會為其加const修飾符,內部無法修改,但是捕獲到的引用可以修改。

[] // 什麼都不捕獲
[=] // 捕獲區域性物件值
[&] // 捕獲區域性物件的引用
[this] // 捕獲this的引用
[a, &b] // 捕獲a的值,捕獲b的引用
auto g = [](int a, int b){ return a + b;};

decltype型別宣告

decltype運算子將會返回傳遞給它的表示式得到的型別。

int a = 0;
decltype(a++) b;
// 以下函式定義方式是錯誤的,因為decltye無法推導函式返回型別,這是因為在函式外部,a和b是未定義
decltype(a + b) Func(int a, int b){
    return a + b;
}

// c++14後支援
auto Func(int a, int b) -> decltype(a + b){
    return a + b;
}
template<typename T>
auto void(T a, T b) -> decltype(T){ // auto推導函式返回值,最好指定返回型別
    return a + b;
}

型別別名

C++using也可以宣告別名,相較於typedef更易讀。

template <typename T, typename T>
using u_map = std::map<T, T>;

相關文章