1. 程式人生 > 其它 >C++11\14\17\20 特性介紹

C++11\14\17\20 特性介紹

轉:https://www.jianshu.com/p/8c4952e9edec

C++11 新特性

#01 auto 與 decltype

auto: 對於變數,指定要從其初始化器⾃動推匯出其型別。⽰例:

auto a = 10;    // 自動推導 a 為 int
auto b = 10.2;  // 自動推導 b 為 double
auto c = &a;    // 自動推導 c 為 int*
auto d = "xxx"; // 自動推導 d 為 const char*

decltype: 推導實體的宣告型別,或表示式的型別。為了解決 auto 關鍵字只能對變數進⾏型別推導的缺陷⽽出現。⽰例:

int a = 0;

decltype(a) b = 1;        // b 被推導為 int 型別
decltype(10.8) c = 5.5;   // c 被推導為 double 型別
decltype(c + 100) d;      // d 被推導為 double

struct { double x; } aa;
decltype(aa.x) y;         // y 被推導為 double 型別
decltype(aa) bb;          // 推斷匿名結構體型別

C++11 中 auto 和 decltype 結合再借助「尾置返回型別」還可推導函式的返回型別。⽰例:

// 利⽤ auto 關鍵字將返回型別後置
template<typename T, typename U>
auto add1(T x, U y) -> decltype(x + y) {
  return x + y;
}

C++14 開始⽀持僅⽤ auto 並實現返回型別推導,見下⽂ C++14 章節。

#02 defaulted 與 deleted 函式

在 C++ 中,如果程式設計師沒有⾃定義,那麼編譯器會預設為程式設計師⽣成 「建構函式」、「拷貝建構函式」、「拷貝賦值函式」 等。

但如果程式設計師⾃定義了上述函式,編譯器則不會⾃動⽣成這些函式。

⽽在實際開發過程中,我們有時需要在保留⼀些預設函式的同時禁⽌⼀些預設函式

例如建立 「不允許拷貝的類」 時,在傳統 C++ 中,我們經常有如下的慣例程式碼:

// 除非特別熟悉編譯器自動生成特殊成員函式的所有規則,否則意圖是不明確的
class noncopyable   {
public:
  // 由於下⽅有⾃定義的建構函式(拷⻉建構函式)
  // 編譯器不再⽣成預設建構函式,所以這⾥需要⼿動定義建構函式
  // 但這種⼿動宣告的建構函式沒有編譯器⾃動⽣成的預設建構函式執⾏效率⾼
  noncopyable() {};
private: 
  // 將拷⻉建構函式和拷⻉賦值函式設定為 private
  // 但卻⽆法阻⽌友元函式以及類成員函式的調⽤
  noncopyable(const noncopyable&);
  noncopyable& operator=(const noncopyable&);
};

傳統 C++ 的慣例處理⽅式存在如下缺陷:

  1. 由於⾃定義了「拷貝建構函式」,編譯器不再⽣成「預設建構函式」,需要⼿動的顯式定義「無參建構函式」
  2. ⼿動顯式定義的「無參建構函式」效率低於「預設建構函式」
  3. 雖然「拷貝建構函式」和「拷貝賦值函式」是私有的,對外部隱藏。但⽆法阻⽌友元函式和類成員函式的調⽤
  4. 除⾮特別熟悉編譯器⾃動⽣成特殊成員函式的所有規則,否則意圖是不明確的

為此,C++11 引⼊了 defaultdelete 關鍵字,來顯式保留或禁止特殊成員函式:

class noncopyable {
 public:
  noncopyable() = default;
  noncopyable(const noncopyable&) = delete;
  noncopyable& operator=(const noncopyable&) = delete;
};

#03 final 與 override

在傳統 C++ 中,按照如下⽅式覆蓋⽗類虛擬函式:

struct Base {
  virtual void foo();
};
struct SubClass: Base {
  void foo();
};

上述程式碼存在⼀定的隱患:

  • 程式設計師並⾮想覆蓋⽗類虛擬函式,⽽是 定義了⼀個重名的成員函式。由於沒有編譯器的檢查導致了意外覆蓋且難以發現
  • ⽗類的虛擬函式被刪除後,編譯器不會進⾏檢查和警告,這可能引發嚴重的錯誤

為此,C++11 引⼊ override 顯式的宣告要覆蓋基類的虛擬函式,如果不存在這樣的虛擬函式,將不會通過編譯:

class Parent {
  virtual void watchTv(int);
};
class Child : Parent {
  virtual void watchTv(int) override;    // 合法
  virtual void watchTv(double) override; // 非法,父類沒有此虛擬函式
};

final 則終⽌虛類被繼承或虛擬函式被覆蓋:

class Parent2 {
  virtual void eat() final;
};

class Child2 final : Parent2 {};  // 合法

class Grandson : Child2 {};       // 非法,Child2 已經 Final,不可被繼承

class Child3 : Parent2 {
  void eat() override; // 非法,foo 已 final
};

#04 尾置返回型別

看一個比較複雜的函式定義:

// func1(int arr[][3], int n) 為函式名和引數
// (* func1(int arr[][3], int n)) 表示對返回值進⾏解引⽤操作
// (* func1(int arr[][3], int n))[3] 表示返回值解引⽤後為⼀個⻓度為 3 的陣列
// int (* func1(int arr[][3], int n))[3] 表示返回值解引⽤後為⼀個⻓度為 3 的 int 陣列
int (* func1(int arr[][3], int n))[3] {
  return &arr[n];
}

C++11 引⼊「尾置返回型別」,將「函式返回型別」通過 -> 符號連線到函式後面,配合 auto 簡化上述複雜函式的定義:

// 返回指向陣列的指標
auto fun1(int arr[][3], int n) -> int(*)[3] {
  return &arr[n];
}

尾置返回型別經常在 「lambda 表示式」、「模板函式返回」中使⽤:

// 使⽤尾置返回型別來宣告 lambda 表示式的返回型別
[capture list] (params list) mutable exception->return_type { function body }

// 在模板函式返回中結合 auto\decltype 宣告模板函式返回值型別
template<typename T, typename U>
auto add(T x, U y) -> decltype(x + y) {
  return x + y;
}

#05 右值引⽤

何為左值與右值
  • 左值:記憶體中有確定儲存地址的物件的表示式的值
  • 右值:所有不是左值的表示式的值。右值可分為「傳統純右值」和「將亡值

上述的「傳統純右值」和「將亡值」又是什麼?

  • 純右值:即 C++11 之前的右值。包括:

    1. 常見的字面量如 0、"123"、或表示式為字面量
    2. 不具名的臨時物件,如函式返回臨時物件
  • 將亡值:隨著 C++11 引入的右值引用而來的概念。包括:

    1. 「返回右值引用的函式」的返回值。如返回型別為 T&& 的函式的返回值
    2. 「轉換為右值引用的轉換函式」的返回值,如 std::move() 函式的返回值

同時,左值 + 將亡值又被稱為「泛左值」。這幾個概念對於剛接觸的同學可能會比較混亂,我們梳理一下,如下圖所示:


value_type.png

左值還是右值可以通過取地址運算子 & 來進⾏判斷,能夠通過 & 正確取得地址的為左值,反之為右值。

int i = 0;
int* p_i = &i;            // 可通過 & 取出地址,固 i 為左值
cout << p_i << endl;

int* p_i_plus = &(i + 1); // 非法,i + 1 為右值
int* p_i_const = &(0);    // 非法,0 為右值
何為左值引用與右值引用

C++11 之前,我們就經常使⽤對左值的引⽤,即左值引⽤,使用 & 符號宣告:

int j = 0;
int& ref_j = j;           // ref_j 為左值引⽤
int& ref_ret = getVal();  // ref_ret 為左值引用

int& ref_j_plus = j + 1;  // ⾮法,左值引⽤不能作⽤於右值
int& ref_const = 0;       // 非法,左值引用不能作用於右值

如上例程式碼所示,ref_j_plusref_const 為傳統 C++ 中經常使用的左值引用,無法作用於 j+10 這樣的右值。

C++11 引⼊了針對右值的引⽤,即右值引⽤,使用 && 符號宣告:

int&& ref_k_plus = (i + 1); // ref_k_plus 為右值引用,它綁定了右值 i + 1
int&& ref_k = 0;            // ref_k 為右值引用,它綁定了右值 0 
右值引用的特點

以下述程式碼為例:

int getVal() {
  return 1;
}

int main() {
  // 這裡存在兩個值:
  //    1. val(左值)
  //    2. getVal() 返回的臨時變數(右值)
  // 其中 getVal() 返回的臨時變數賦值給 val 後會被銷燬
  int val = getVal();
  return 0;
}

上述程式碼中,getVal 函式產⽣的 「臨時變數」 需要先複製給左值 val,然後再被銷燬。

但是如果使⽤右值引⽤:

// 使用 && 來表明 val 的型別為右值引用
// 這樣 getVal() 返回的臨時物件(右值) 將被「續命」
// 擁有與 val 一樣長的生命週期
int&& val = getVal();

上述程式碼體現了右值引⽤的第⼀個特點

通過右值引⽤的宣告,右值可「重⽣」,⽣命週期與右值引⽤型別的變數⽣命週期⼀樣長。

再看如下例⼦:

template<typename T>
void f(T&& t) {}

f(10);      // t 為右值

int x = 10;
f(x);       // t 為左值

上述例⼦體現了右值引⽤的第⼆個特點

⾃動型別推斷(如模板函式等)的場景下,T&& t 是未定的引⽤型別,即 t 並⾮⼀定為右值。如果它被左值初始化,那麼 t 就為左值。如果它被右值初始化,則它為右值。

正是由於上述特點,C++11 引入右值引⽤可以實現如下⽬的:

  • 實現移動語義。解決臨時物件的低效率拷貝問題
  • 實現完美轉發。解決函式轉發右值特徵丟失的問題
右值引⽤帶來的移動語義

在 C++11 之前,臨時物件的賦值採⽤的是低效的拷貝。

舉例來講,整個過程如同將⼀個冰箱⾥的⼤象搬到另⼀個冰箱,傳統 C++ 的做法是第⼆個冰箱⾥複製⼀個⼀摸⼀樣的⼤象,再把第⼀個冰箱的⼤象銷燬,這顯然不是⼀個⾃然的操作⽅式。

看如下例⼦:

class HasPtrMem1 {
 public:
  HasPtrMem1() : d(new int(0)) {}
  ~HasPtrMem1() { delete d; }
  int* d;
};

int main() {
  HasPtrMem1 a1;
  HasPtrMem1 b1(a1);

  cout << *a1.d << endl;
  cout << *b1.d << endl;

  return 0;
}

上述程式碼中 HasPtrMem1 b(a) 將調⽤編譯器預設⽣成的「拷貝建構函式」進⾏拷貝,且進⾏的是按位拷貝(淺拷貝),這將導致懸掛指標問題[1]

懸掛指標問題[1]: 上述程式碼在執⾏ main 函式後,將銷燬 a、b 物件,於是調⽤對應的解構函式執⾏ delete d 操作。但由 於 a、b 物件中的成員 d 指標同⼀塊記憶體,於是在其中⼀個物件被析構後,另⼀個物件中的指標 d 不再指向有效記憶體,這個物件的 d 就變成了懸掛指標。

在懸掛指標上釋放記憶體將導致嚴重的錯誤。所以針對上述場景必須進⾏深拷貝:

class HasPtrMem2 {
 public:
  HasPtrMem2() : d(new int(0)) {}
  HasPtrMem2(const HasPtrMem2& h) :
      d(new int(*h.d)) {}
  ~HasPtrMem2() { delete d; }
  int* d;
};

int main() {
  HasPtrMem2 a2;
  HasPtrMem2 b2(a2);

  cout << *a2.d << endl;
  cout << *b2.d << endl;

  return 0 ;
}

在上述程式碼中,我們⾃定義了拷貝建構函式的實現,我們通過 new 分配新的記憶體實現了深度拷貝,避免了「懸掛指標」的問題,但也引出了新的問題。

拷貝建構函式為指標成員分配新的記憶體並進⾏拷貝的做法是傳統 C++ 程式設計中是⼗分常見的。但有些時候我們並不需要這樣的拷貝:

HasPtrMem2 GetTemp() {
  return HasPtrMem2();
}

int main() {
  HasPtrMem2 a = GetTemp();
}

上述程式碼中,GetTemp 返回的臨時物件進⾏深度拷貝操作,然後再被銷燬。如下圖所⽰:

copy_constructor.png

如果 HasPtrMem2 中的指標成員是複雜和龐⼤的資料型別,那麼就會導致⼤量的效能消耗。

再回到⼤象移動的類⽐,其實更⾼效的做法是將⼤象直接從第⼀個冰箱拿出,然後放⼊第⼆個冰箱。同樣的,我們在將臨時物件賦值給某個變數時是否可以不⽤拷貝建構函式?答案是肯定的,如下圖所⽰:

move_constructor.png

在 C++11 中,像這樣「偷⾛」資源的建構函式,稱為 「移動建構函式」,這種「偷」的⾏為,稱為 「移動語義」,可理解為「移為⼰⽤」。

當然實現時需要在程式碼中定義對應的「移動建構函式」:

class HasPtrMem3 {
  public:
    HasPtrMem3() : d(new int(0)) {}
    HasPtrMem3(const HasPtrMem3& h) : 
        d(new int(*h.d)) {}
    HasPtrMem3(HasPtrMem3&& h) : d(h.d) {
      h.d = nullptr;
    }
    ~HasPtrMem3() { delete d; }
    int* d;
};

注意「移動建構函式」依然會存在懸掛指標問題,所以在通過移動建構函式「偷」完資源後,要把臨時物件的 h.d 指標置為空,避免兩個指標指向同⼀個記憶體,在析構時被析構兩次。

「移動建構函式」中的引數為 HasPtrMem3&& h 為右值型別[2],⽽返回值的臨時物件就是右值型別,這也是為什麼返回值臨時物件能夠匹配到「移動建構函式」的原因。

右值型別[2]: 注意和上⾯提到的右值引⽤第⼆個特點做區分,這⾥不是型別推導的場景,HasPtrMem3 是確定的型別,所以 HasPtrMem3&& h 就是確定的右值型別。

上述的移動語義是通過右值引⽤來匹配臨時值的,那麼左值是否可以藉助移動語義來優化效能呢?C++11 為我們 提供了 std::move 函式來實現這⼀⽬標:

{
  std::list<std::string> tokens;              // tokens 為左值
  // 省略初始化...
  std::list<std::string> t = tokens;          // 這裡存在拷貝
}

std::list<std::string> tokens;
std::list<std::string> t = std::move(tokens); // 這裡不存在拷貝

std::move 函式實際沒有移動任何資源,它唯⼀做的就是將⼀個左值強制轉換成右值引⽤,從而匹配到「移動建構函式」或「移動賦值運算子」,應⽤移動語義實現資源移動。⽽ C++11 中所有的容器都實現了移動語義,所以使用了 list 容器的上述程式碼能夠避免拷貝,提⾼效能。

右值引⽤帶來的完美轉發

傳統 C++ 中右值引數後被轉換成左值,即不能按照引數原先的型別進⾏轉發,如下所⽰:

template<typename T>
void forwardValue1(T& val) {
  // 右值引數變為左值
  processValue(val);
}

template<typename T>
void forwardValue1(const T& val) {
  processValue(val); // 引數都變成常量左值引用了
}

如何保持引數的左值、右值特徵,C++11 引⼊了 std::forward,它將按照引數的實際型別進⾏轉發:

void processValue(int& a) {
  cout << "lvalue" << endl;
}

void processValue(int&& a) {
  cout << "rvalue" << endl;
}

template<typename T>
void forwardValue2(T&& val) {
  // 照引數本來的型別進⾏轉發
  processValue(std::forward<T>(val));
}

int main() {
  int i = 0;

  forwardValue2(i); // 傳入左值,函式執行輸出 lvalue
  forwardValue2(0); // 傳入右值,函式執行輸出 rvalue

  return 0;
}

#06 移動建構函式與移動賦值運算子

在規則 #05 已經提及,不再贅述。

#07 有作⽤域列舉

傳統 C++ 的列舉型別存在如下問題:

  • 每⼀個列舉值在其作⽤域內都是可見,容易引起命名衝突
// Color 下的 BLUE 和 Feeling 下的 BLUE 命名衝突
enum Color { RED, BLUE };
enum Feeling { EXCITED, BLUE };
  • 會被隱式轉換成 int,這在那些不該轉換成 int 的場景下可能導致錯誤
  • 不可指定列舉的資料型別,導致程式碼不易理解、不可進⾏前向宣告等

在傳統 C++ 中也有⼀些間接⽅案可以適當解決或緩解上述問題,例如使⽤名稱空間

namespace Color { enum Type { RED, YELLOW, BLUE }; };

或使⽤類、結構體:

struct Color { enum Type { RED, YELLOW, BLUE }; };

但上述⽅案通常值解決了作⽤域問題,隱式轉換以及資料型別的問題⽆法解決。

C++11 引⼊了列舉類解決上述問題:

// 定義列舉值為 char 型別的列舉類
enum class Color:char { RED, BLACK };

// 使⽤
Color c = Color::RED;

#08 constexpr 與字⾯型別

constexpr: 在編譯期將表示式或函式編譯為常量結果

constexpr 修飾變數、函式:

// 修飾變數
constexpr int a = 1 + 2 + 3;
char arr[a]; // 合法,a 是編譯期常量

// 修飾函式,使函式在編譯期會成為常量表達式(如果可以)
// 如果 constexpr 函式返回的值不能在編譯器確定,則 constexpr 函式就會退化為執行期函式(這樣做的初衷是避免在為編譯期和執行期寫兩份相同程式碼)
// constexpr 函式的設計其實不夠嚴謹,所以 C++20 引入了 consteval (詳見下文 C++20 部分)
// C++11 中,constexpr 修飾的函式只能包含 using 指令、typedef 語句以及 static_assert 
// C++14 實現了對其他語句的支援
constexpr int len_foo_constexpr() {
  return 5;
}

#09 初始化列表 - 擴充套件「初始化列表」的適⽤範圍

在 C++98/03 中,普通陣列或 POD 型別 可以通過初始化列表的⽅式進⾏初始化,例如:

POD 型別見下文的 #18 條

int arr1[3] = { 1, 2, 3 };

long arr2[] = { 1, 3, 2, 4 };
struct A { 
  int x;
  int y;
} a = { 1, 2 };

C++11 擴充套件了「初始化列表」的適⽤範圍,使之可以適⽤於所有型別物件的初始化:

class Dog {
 public:
  Dog(string name, int age) {
    cout << name << " "; cout << age << endl;
  }
};

Dog dog1 = {"cat1", 1};
Dog dog2 {"cat2", 2};

還可以通過 std::initializer_list 來實現更強⼤的「初始化列表」,例如:

class Dog {
 public:
  Dog(initializer_list<int> list) {
   for (initializer_list<int>::iterator it = list.begin();
          it != list.end(); ++it) {
      cout << *it << " ";
    } 
    cout << endl;
  }
};

Dog dog3 = {1, 2, 3, 4, 5};

同時,初始化列表還可以⽤作普通函式的形參返回值

// 形參
void watch(Dog dog) {
  cout << "watch" << endl;
}

watch({"watch_dog", 4});

// Dog 作為返回值

getDefaultDog() {
  return {"default", 3};
}

getDefaultDog();

#10 委託與繼承的建構函式

委託構造:在⼀個建構函式中調⽤同⼀個類的另⼀個建構函式
繼承構造:在 C++11 之前的 C++ 中,⼦類需要依次宣告⽗類擁有的建構函式,並傳遞相應的初始化引數。C++11 利⽤關鍵字 using 引⼊了繼承建構函式,使⽤⼀⾏語句讓編譯器⾃動完成上述⼯作。

class Parent {
  public:
    int value1;
    int value2;

  Parent() {
    value1 = 1;
  }

  Parent(int value) : Parent() { // 委託 Parent() 建構函式
    value2 = value;
  }
}

class Child : public Parent {
  public: 
    using Parent::Parent;         // 繼承構造
}

#11 花括號或等號初始化器

上⽂已提及,不再贅述

#12 nullptr

傳統 C++ 中 NULL 的定義存在很多缺陷,編譯器在實現時常常將其定義為 0,這會導致過載混亂。考慮如下程式碼;

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

當調⽤ foo(NULL) 時將匹配到 foo(int) 函式,這顯然會讓⼈感到迷惑。

C++11 引⼊了 nullptr (型別為 nullptr_t)關鍵字,以便區分空指標與 0,且 nullptr 能夠隱式的轉換為任何指標或成員指標的型別。

#13 long long

long: ⽬標型別將有⾄少 32 位的寬度
long long: ⽬標型別將有⾄少 64 位的寬度

如同 long 型別字尾需要 「l」 或 「L」,long long 型別字尾需要加上「ll」或「LL」。

#14 char16_t 與 char32_t

C++98 中為了表達 Unicode 字串,引⼊了 wchar_t 型別,以解決 1 位元組的 char 只能 256 個字元的問題。

但是由於 wchar_t 型別在不同平臺上實現的長度不同,在程式碼移植⽅⾯有⼀定的影響。於是 C++11 引⼊ char16_tchar32_t,他們擁有的固定的長度,分別為 2 個位元組4 個位元組

char16_t: UTF-16 字元表⽰的型別,要求⼤到⾜以表⽰任何 UTF-16 編碼單元( 16 位)。它與 std::uint_least16_t 具有相同的⼤⼩、符號性和對齊,但它是獨⽴的型別。

char32_t: - UTF-32 字元表⽰的型別,要求⼤到⾜以表⽰任何 UTF-32 編碼單元( 32 位)。它與 std::uint_least32_t 具有相同的⼤⼩、符號性和對齊,但它是獨⽴的型別。

同時 C++11 還定義了 3 個常量字串字首:

  • u8 代表 UTF-8 編碼
  • u 代表 UTF-16 編碼
  • U 代表 UTF-32 編碼
char16_t UTF16[] = u"中國"; // 使用 UTF-16 編碼儲存

char32_t UTF16[] = U"中國"; // 使用 UTF-32 編碼儲存

#15 類型別名

傳統 C++ 中使⽤ typedef 來為型別定義⼀個新的名稱,C++11 中我們可以使⽤ using 達到同樣的效果,如下所⽰:

typedef std::ios_base::fmtflags flags;
using flags = std::ios_base::fmtflags;

既然有了 typedef 為什麼還引⼊ using?當然是因為 using ⽐起 typedef 還能做更多。

typedef 是隻能為「型別」定義新名稱,⽽模板則是 「⽤來產⽣型別」的,所以以下程式碼是⾮法的:

template<typename T, typename U>
class DogTemplate {
  public: 
    T attr1;
    U aatr2;
};

// 不合法
template<typename T>
typedef DogTemplate<std::vector<T>, std::string> DogT;

但使⽤ using 則可以為模板定義別名:

template<typename T>
using DogT = DogTemplate<std::vector<T>, std::string>;

#16 變長引數模板

在傳統 C++ 中,類模板或函式模板只能接受固定數量的模板引數。

⽽ C++11 允許任意多個、任意類別的模板引數,同時在定義時⽆需固定引數個數。如下所⽰:

template<typename... T> class DogT;

// 傳⼊多個不同型別的模板引數
class DogT<int, 
            std::vector<int>,
            std::map<std::string,
            std::vector<int>>> dogT;

// 不傳⼊引數( 0 個引數)
class DogT<> nothing;

// 第⼀個引數必傳,之後為變⻓引數
template<typename require, typename... Args> class CatT;

同樣的可⽀持模板函式:

template<typename... Args>
void my_print(const std::string& str, Args... args) {
  // 使⽤ sizeof... 計算引數個數
  std::cout << sizeof...(args) << std::endl;
}

#17 推⼴的(⾮平凡)聯合體

聯合體 Union 為我們提供了在⼀個結構內定義多種不同型別的成員的能⼒,但在傳統 C++ 中,並不是所有的資料型別都能成為聯合體的資料成員。例如:

struct Dog {
  Dog(int a, int b) : age(a), size(b) {}
  int age;
  int size;
}

union T {
  // C++11 之前為非法(d 不是 POD 型別)
  // C++11 之後合法
  Dog d;
  int id;
}

有關 POD 型別參考下⽂的 #18 條

C++11 去除了上述聯合體的限制[3],標準規定了任何⾮引⽤型別都可以成為聯合體的資料成員

[3] 去除的原因是經過長期的實踐證明為了相容 C 所做的限制沒有必要。

#18 推⼴的 POD (平凡型別與標準佈局型別)

POD 為 Plain Old Data 的縮寫,Plain 突出其為⼀種普通資料型別,Old 體現其具有與 C 的相容性,例如可以使⽤ memcpy() 函式進⾏複製、使⽤ memset() 函式進⾏初始化等。

具體地,C++11 將 POD 劃分為兩個概念的合集:平凡的(trival)和標準佈局的(standard layout)。

其中平凡的類或結構體應該符合如下要求:

  1. 擁有平凡的預設建構函式和解構函式。即不⾃定義任何建構函式,或通過 =default 來顯⽰指定使⽤預設建構函式
  2. 擁有平凡的拷貝建構函式和移動建構函式
  3. 擁有平凡的拷貝賦值運算子和移動賦值運算子
  4. 不包含虛擬函式以及虛基類

C++11 同時提供了輔助類模板 is_trivial 來實現是否平凡的判斷:

cout << is_trivial<DogT>::value << endl;

POD 包含的另⼀個概念則是「標準佈局」。標準佈局的類或結構體需要符合如下要求:

  1. 所有⾮靜態成員有相同的訪問許可權(public、private、protected)
  2. 類或結構體繼承時滿⾜如下兩個條件之⼀:
    2.1 ⼦類中有⾮靜態成員,且只有⼀個僅包含靜態成員的基類
    struct B1 { static int a; };
    struct B2 { static int b; };
    
    2.2 基類有⾮靜態成員,則⼦類沒有⾮靜態成員
    struct B2 { int a; } ;
    struct D2 : B2 { static int d; };
    
    從上述條件可知,1. 只要⼦類和基類同時都有⾮靜態成員 2. ⼦類繼承多個基類,有多個基類同時有⾮靜態成員。 這兩種情況都不屬於標準佈局。
  3. 類中第⼀個⾮靜態成員的型別與其基類不同
struct A : B { B b; };        // 非標準佈局,第一個非靜態成員 b 就是基本型別
struct A : B { int a; B b; }; // 標準佈局,第一個非靜態成員 a 不是基類 B 型別
  1. 沒有虛擬函式或虛基類
  2. 所有⾮靜態資料成員均符合標準佈局型別,其基類也符合標準佈局(遞迴定義)

同樣 C++11 提供了輔助類模板 is_standard_layout 幫助我們判斷:

cout << is_standard_layout<Dog>::value << endl;

最後,C++11 也提供了⼀次性判斷是否為 POD 的輔助類模板 is_pod:

cout << is_pod<Dog>::value << endl;

瞭解 POD 的基本概念,POD 到底有怎樣的作⽤或好處呢?POD 能夠給我們帶來如下優點:

  1. 位元組賦值。安全的使⽤ memset 和 memcpy 對 POD 型別進⾏初始化和拷貝等操作
  2. 相容 C 記憶體佈局。以便與 C 函式進⾏互操作
  3. 保證靜態初始化的安全。⽽靜態初始化可有效提⾼程式效能

#19 Unicode 字串字⾯量

在 #14 已有所提及,C++11 定義了 3 個常量字串字首:

  • u8 代表 UTF-8 編碼
  • u 代表 UTF-16 編碼
  • U 代表 UTF-32 編碼

另外 C++11 還引⼊了⼀個字串字首 R 表⽰ 「原⽣字串字⾯量」,所謂「原⽣字串字⾯量」即表⽰字串⽆需通過轉義處理特殊字元,所見即所得:

// ⽤法: R"分隔符 (原始字元 )分隔符"
string path = R"(D:\workspace\vscode\java_demo)";

// - 作為分隔符,
// 因為原始字串含有 )",如果不新增 - 作為分隔符,則會導致字串錯誤標示結束位置
// 分隔符應該儘量使用原始字串中未出現的字元,以便正確標示開始與結尾
string path2 = R"-(a\b\c)"\daaa\e)-";

#20 ⽤戶定義字⾯量

⽤戶定義字⾯量即⽀持⽤戶定義型別的字⾯量。

傳統 C++ 提供了多種字⾯量,例如 "12.5" 為⼀個 double 型別字⾯量。"12.5f" 為⼀個 float 型別字⾯量。這些字⾯量是 C++ 標準中定義和規定的字⾯量,程式和⽤戶⽆法⾃定義新的字⾯量型別字尾

C++11 則是引⼊了⽤戶⾃定義字⾯量的能⼒。主要通過定義「字⾯量運算子函式」或函式模板實現。該運算子名稱由⼀對相鄰雙引號前導。字⾯量運算子通常在⽤戶定義字⾯量的地⽅被隱式調⽤。例如:

struct S {
  int value;
};

// 使用者定義字面量運算子的實現
S operator ""_mysuffix(unsigned long long v) {
  S s_;
  S_.value = (int) v;
  return s_;
}

// 使用
S sv;
// 101 為型別為 S 的字面量
// _mysuffix 是我們自定義的字尾,如同 float 的 f 一般
sv = 101_mysuffix;

⽤戶⾃定義字⾯量通常由以下⼏種類型:

  1. 數值型字面量
    1.1 整數型字面量
    1.2 浮點型字面量
OutputType operator "" _suffix(unsigned long long);
OutputType operator "" _suffix(long double);
 
// Uses the 'unsigned long long' overload.
OutputType some_variable = 1234_suffix;
// Uses the 'long double' overload.
OutputType another_variable = 3.1416_suffix; 
  1. 字串字面量
OutputType operator "" _ssuffix(const char     * string_values, size_t num_chars);
OutputType operator "" _ssuffix(const wchar_t  * string_values, size_t num_chars);
OutputType operator "" _ssuffix(const char16_t * string_values, size_t num_chars);
OutputType operator "" _ssuffix(const char32_t * string_values, size_t num_chars);

// Uses the 'const char *' overload.
OutputType some_variable =   "1234"_ssuffix; 
// Uses the 'const char *' overload.
OutputType some_variable = u8"1234"_ssuffix;
// Uses the 'const wchar_t *'  overload. 
OutputType some_variable =  L"1234"_ssuffix; 
// Uses the 'const char16_t *' overload.
OutputType some_variable =  u"1234"_ssuffix; 
// Uses the 'const char32_t *' overload.
OutputType some_variable =  U"1234"_ssuffix; 
  1. 字元字面量
S operator "" _mysuffix(char value) {
  const char cv[] {value,'\0'};
  S sv_ (cv);
  return sv_;
}

S cv {'h'_mysuffix};

盡整些花裡胡哨的特性

#21 屬性

C++11 引⼊了所謂的 「屬性」來讓程式設計師在程式碼中提供額外資訊,例如:

// f() 永不返回
void f [[ noreturn ]] () {
  throw "error";  // 雖然不能返回,但可以丟擲異常
}

上述例⼦的展現了屬性的基本形式,noreturn 表⽰該函式永不返回。

C++11 引⼊了兩個屬性:

屬性版本修飾⽬標作⽤
noreturn C++11 函式 指⽰函式不返回,沒有return語句,不正常執⾏完畢,但是可以通過出異常或 者exit()函式退出
carries_dependency C++11 函式、變數 指⽰釋放消費 std::memory_order 中的依賴鏈傳⼊和傳出該函式

概念與功能與 Java 中的註解有些類似

#22 Lambda 表示式

Lambda 表示式基本語法:

// [捕獲列表]:捕獲外部變數,詳見下文
// (引數列表): 函式引數列表
// mutable: 是否可以修改值捕獲的外部變數
// 異常屬性:exception 異常宣告
[捕獲列表](引數列表) mutable( 可選 ) 異常屬性 -> 返回型別 {
  // 函式體
}

例如:

bool cmp(int a, int b) {
  return a < b;
}

int main() {
  int x = 0;
  // 傳統做法
  sort(vec.begin(), vec.end(), cmp);

  // 使用 lambda
  sort(vec.begin(), vec.end(), [x](int a, int b) -> bool { return a < b; });
  return 0;
}

lambda 表示式中的「捕獲列表」可以讓 lambda 表示式內部使用其可見範圍的外部變數,例如上例中的 x。捕獲列表一般有以下幾種型別:
1. 值捕獲
與引數傳遞中值傳遞類似,被捕獲的變數以值拷貝的方式傳入:

int a = 1;
auto f1 = [a] () { a+= 1; cout << a << endl;};

a = 3;
f1();

cout << a << endl;

2. 引用捕獲
加上 & 符號,即可通過引用捕獲外部變數:

int a = 1;
// 引用捕獲
auto f2 = [&a] () { cout << a << endl; };

a = 3;
f2();

3. 隱式捕獲
無需顯示列出所有需要捕獲的外部變數,通過 [=] 可以通過「值捕獲」的方式捕獲所有外部變數,[&] 可以通過「引用捕獲」的方式捕獲所有外部變數:

int a = 1;
auto f3 = [=] { cout << a << endl; };    // 值捕獲
f3(); // 輸出:1

auto f4 = [&] { cout << a << endl; };    // 引用捕獲
a = 2;
f4(); // 輸出:2

4. 混合方式
以上方式的混合,[=, &x] 表示變數 x 以引用形式捕獲,其餘變數以傳值形式捕獲。

最終 lambda 捕獲外部變數總結如下表所示:

捕獲形式說明
[] 不捕獲任何外部變數
[變數名, …] 預設以值得形式捕獲指定的多個外部變數(用逗號分隔),如果引用捕獲,需要顯示宣告(使用&說明符)
[this] 以值的形式捕獲this指標
[=] 以值的形式捕獲所有外部變數
[&] 以引用形式捕獲所有外部變數
[=, &x] 變數x以引用形式捕獲,其餘變數以傳值形式捕獲
[&, x] 變數x以值的形式捕獲,其餘變數以引用形式捕獲

#23 noexcept 說明符與 noexcept 運算子

C++11 將異常的宣告簡化為以下兩種情況:

  1. 函式可能丟擲任何異常
void func(); // 可能丟擲異常
  1. 函式不可能丟擲任何異常
void func() noexcept; // 不可能丟擲異常

使⽤ noexcept 能夠讓編譯器更好的優化程式碼,同時 noexcept 修飾的函式如果丟擲異常將會導致調⽤ std::terminate() ⽴即終⽌程式。

noexcept 還可作為運算子使⽤,來判斷⼀個表示式是否產⽣異常:

cout << noexcept(func()) << endl;

#24 alignof 與 alignas

C++11 引⼊了 alignofalignas 來實現對記憶體對齊的控制。

alignof: 能夠獲取對齊⽅式
alignas: ⾃定義結構的對齊⽅式:

struct A {
  char a;
  int b;
};

struct alignas(std::max_align_t) B {
  char a;
  int b;
  float c;
};

cout << alignof(A) << endl;
cout << alignof(B) << endl;

#25 多執行緒記憶體模型

請參見 LevelDB 中的跳錶實現 中的 「C++ 中的 atomic 和 memory_order」一節。

#26 執行緒區域性儲存

在多執行緒程式中,全域性以及靜態變數會被多個執行緒共享,這在某些場景下是符合期望和需求的。

但在另⼀些場景下,我們希望能有執行緒級的變數,這種變數是執行緒獨享的,不受其他執行緒影響。我們稱之為執行緒區域性儲存(TLS, thread local storage)

C++11 引⼊了 thread_local ⽤來宣告執行緒區域性儲存,如下所⽰:

int thread_local num;

#27 GC 介面

眾所周知 C++ 是⼀門顯式堆記憶體管理的語⾔,程式設計師需要時時刻刻關注⾃⼰對記憶體空間的分配和銷燬。 ⽽如果程式設計師沒有正確進⾏堆記憶體管理,就會造成程式的異常、錯誤、崩潰等。從語⾔層⾯是講,這些不正確的記憶體管理主要有:

  • 野指標:記憶體已經被銷燬,但指向它的指標依然被使⽤
  • 重複釋放:釋放已經被釋放過的記憶體,或者釋放被重新分配過的記憶體,導致重複釋放錯誤
  • 記憶體洩漏:程式中不再需要的記憶體空間卻沒有被及時釋放,導致隨著程式不斷運⾏記憶體不斷被⽆謂消耗

顯式記憶體管理可以為程式設計師提供極⼤的程式設計靈活性,但也提⾼了出錯的概率。為此,C++11 進⼀步改造了智慧指標,同時也提供了⼀個 「最⼩垃圾回收」的標準。

⽬前⾮常多的現代語⾔都全⾯⽀持「垃圾回收」,例如 Java、Python、C#、Ruby、PHP 等都⽀持「垃圾回收」。 為實現「垃圾回收」,最重要的⼀點就是判斷物件或記憶體何時能夠被回收。判斷物件或記憶體是否可回收的⽅法主要有:

  1. 引用計數
  2. 跟蹤處理(跟蹤物件關係圖)。如 Java 中的「物件可達性分析」。

確定了物件或記憶體可被回收後,就需要進⾏回收,⽽這⾥又存在不同的回收策略和回收演算法(簡單描述):

  1. 標記-清除
    第⼀步對物件和記憶體進⾏標記是否可回收,第⼆步對標記的記憶體進⾏回收。顯然這種⽅法將導致⼤量的記憶體碎⽚
  2. 標記-整理
    第⼀步同樣是標記。但是第⼆步不是直接清理,⽽是將「活物件」向左靠齊(整理)。但移動⼤量物件,將導致程 序中的引⽤需要進⾏更新。如果物件死亡的⽐較多,就要進⾏⽐較多的移動操作。所以適合「長壽」的物件。
  3. 複製演算法。將堆空間分為兩個部分:fromto。from 空間⽤滿後啟動掃描標記,找出其中活著的物件,將其複製到 to 空間, 然後清空 from 空間。之後原先 to 變成了 from 空間供程式分配記憶體,原先的 from 變成 to,等待下⼀次垃圾回收收容那些「倖存者」。如果有⼤量倖存者,那麼拷貝將導致較⼤效能消耗。因此適合短壽「朝⽣暮死」的物件。

⽽在實現時通常採⽤「分代收集」演算法,即將堆空間分為 「新⽣代」「⽼年代」,新⽣代朝⽣暮死適合「拷貝演算法」,⽼年代長壽適合「標記清理」或「標記整理」。

上述介紹了垃圾回收的相關演算法,C++11 則是制定了「最⼩垃圾回收」的標準,所謂「最⼩」指的其實就是它壓根就不是⼀個完整的 GC,⽽是為了後續的 GC 鋪墊,⽬前也只是提供了⼀些庫函式來輔助 GC,如:
declare_reachable(宣告⼀個物件不能被回收)、undeclare_reachable(宣告⼀個物件可以被回收)。

由於 C++ 中的指標⼗分靈活,這種靈活性將導致 GC 誤判從⽽回收記憶體,因此提供這兩個函式保護物件:

int* p1 = new int(1);
p1 += 10;             // 將導致 GC 回收記憶體空間
p1 -= 10;             // 指標的靈活性:又移動回來了
*p1 = 10;             // 記憶體已被回收,導致程式錯誤

// 使用 declare_reachable 保護物件不被 GC
int* p2 = new int(2);
declare_reachable(p2); // p2 不可回收

p2 += 10;              // GC 不會回收
p2 -= 10;

*p2 = 10;              // 程式正常

從上可知,這兩個函式就是為舊程式相容即將到來[4]的 C++ GC 而設計的。

[4] 看樣子是不會到來了。

上述介紹了這麼多,最後再來介紹最尷尬的⼀點:現在還沒有編譯器實現 C++11 有關 GC 的標準

可以暫時忽略這條 GC 特性,實際上 C++ 的很多特性都可以忽略

#28 範圍 for

類似 Java 中的 foreach 迴圈

std::vector<int> vec = {1, 2, 3, 4};
for (auto element : vec) {
  std::cout << element << std::endl;  // read only
}

#29 static_assert

我們常⽤ assert,即運⾏時斷⾔。但很多事情不該在運⾏時採取判斷和檢查,而應該在編譯期就進⾏嚴格斷⾔,例如陣列的長度等。

C++11 引⼊了 static_assert 實現編譯期斷⾔:

static_assert(sizeof(void *) == 4,"64位系統不支援");

#30 智慧指標

C++98 提供了模板型別「auto_ptr」來實現智慧指標。auto_ptr 以物件的⽅式管理分配的記憶體,並在適當的時機釋放記憶體。程式設計師只需要將 new 操作返回的指標作為 auto_ptr 的初始值即可,如下所⽰:

auto_ptr(new int);

但 auto_ptr 存在「進⾏拷貝時會將原指標置為 NULL」等缺陷,因此 C++11 引⼊了 unique_ptr、shared_ptr、 weak_ptr 三種智慧指標。

  • unique_ptr: unique_ptr 和指定物件的記憶體空間緊密繫結,不允許與其他 unique_ptr 指標共享同⼀個物件記憶體。即記憶體所有權在同⼀個時間內是唯⼀的,但所有權卻可以通過 #05 條中提及的 move 和移動語義進⾏來實現「所有權」 轉移。如下所⽰:
unique_ptr<int> p1(new int(111));

unique_ptr<int> p2 = p1;        // ⾮法,不可共享記憶體所有權
unique_ptr<int> p3 = move(p1);  // 合法,移交所有權。p1 將喪失所有權

p3.reset();                     // 顯式釋放記憶體
  • shared_ptr:與 unique_ptr 相對,可以共享記憶體所有權,即多個 shared_ptr 可以指向同⼀個物件的記憶體。同時 shared_ptr 採⽤引⽤計數法來判斷記憶體是否還被需要,從⽽判斷是否需要進⾏回收。
shared_ptr<int> p4(new int(222));
shared_ptr<int> p5 = p4;  // 合法

p4.reset();               // 「釋放」記憶體

// 由於採⽤引⽤計數法,p4.reset() 僅僅使得引⽤數減⼀
// 所指向的記憶體由於仍有 p5 所指向,所以不會被回收
// 訪問 *p5 是合法且有效的
cout << *p5 << endl;      // 輸出 222
  • weak_ptr:weak_ptr 可以指向 shared_ptr 指向的記憶體,且在必要時可以通過成員 lock 來返回⼀個指向當前記憶體的 shared_ptr 指標,如果當前記憶體已經被釋放,那麼將 lock() 返回 nullptr。⽽另⼀個重點則是 weak_ptr 不參與引⽤計數。如同⼀個「虛擬指標」⼀樣指向 shared_ptr 指向的物件記憶體,⼀⽅⾯不妨礙記憶體的釋放,另⼀⽅⾯又可以通過 weak_ptr 判斷記憶體是否有效以及是否已經被釋放:
shared_ptr<int> p6(new int(333));
shared_ptr<int> p7 = p6;
weak_ptr<int> weak_p8 = p7;

shared_ptr<int> p9_from_weak_p8 = weak_p8.lock();

if (p9_from_weak_p8 != nullptr) {
  cout << "記憶體有效" << endl;
} else {
  cout << "記憶體已被釋放" << endl;
}

p6.reset();
p7.reset(); // weak_p8

// 記憶體已被釋放,即使 weak_p8 還「指向」該記憶體

weak_ptr 還有⼀個⾮常重要的應⽤並是解決 shared_ptr 引⽤計數法所帶來的 「迴圈引⽤」問題。所謂「迴圈引⽤」 如下圖所⽰:

pointer1.png

由於 ObjA 和 ObjB 內部有成員變數相互引⽤,即使將 P1 和 P2 引⽤去除,這兩個物件的引⽤計數仍然不為 0。但實際上兩個物件已經不可訪問,理應被回收。

使⽤ weak_ptr 來實現上⾯兩個物件的相互引⽤則可以解決該問題,如下圖所⽰:

pointer2.png

將 P1 和 P2 引⽤去除,此時 ObjA 和 ObjB 內部是通過 weak_ptr 相互引用的,由於 weak_ptr 不參與引用計數,因此 ObjA 和 ObjB 的引用計數被判斷為 0,ObjA 和 ObjB 將被正確回收。

C++14 新特性

#01 變數模板

我們已經有了類模板、函式模板,現在 C++14 為我們帶來了變數模板:

template<class T>
constexpr T pi = T(3.1415926535897932385);

int main() {
  cout << pi<int> << endl;

  cout << pi<float> << endl;

  cout << pi<double> << endl;

  return 0;
}

// 當然在以前也可以通過函式模板來模擬
// 函式模板
template<class T>
constexpr T pi_fn() {
  return T(3.1415926535897932385);
}

#02 泛型 lambda

所謂「泛型 lambda」,就是在形參宣告中使用 auto 型別指示說明符的 lambda。例如:

auto lambda = [](auto x, auto y) { return x + y; };

#03 lambda 初始化捕獲

C++11 lambda 已經為我們提供了值捕獲和引⽤捕獲,但針對的實際都是左值,⽽右值物件⽆法被捕獲,這個問題在 C++14 中得到了解決:

int a = 1;
auto lambda1 = [value = 1 + a] {return value;};

std::unique_ptr ptr(new int(10));

// 移動捕獲
auto lambda2 = [value = std::move(ptr)] {return *value;};

#04 new/delete elision

不知怎麼翻譯好,new/delete 消除?new/delete 省略?
cppreference c++14 列出了這條,但沒有詳細說明。

由於 C++14 新提供了 make_unique 函式,unique_ptr 可在析構是自動刪除,再加上 make_shared 和 shared_ptr,基本可以覆蓋大多數場景和需求了。所以從 C++14 開始, new/delete 的使用應該會大幅度減少。

#05 constexpr 函式上放鬆的限制

在 C++11 的 #08 條中已經提及 constexpr 修飾的函式除了可以包含 using 指令、typedef 語句以及 static_assert 斷⾔ 外,只能包含⼀條 return 語句。

⽽ C++14 則放開了該限制,constexpr 修飾的函式可包含 if/switch 等條件語句,也可包含 for 迴圈

#06 ⼆進位制字⾯量

C++14 的數字可⽤⼆進位制形式表達,字首使⽤ 0b0B

int a = 0b101010; // C++14

#07 數字分隔符

使⽤單引號 ' 來提⾼數字可讀性:

auto integer_literal = 100'0000;

GC、模組、協程等重大特性唯唯諾諾,可有可無的特性 C++ 重拳出擊!

#08 函式的返回型別推導

上文提及了 C++11 中使用 auto/decltype 配合尾置返回值實現了函式返回值的推導,C++14 實現了一個 auto 並自動推導返回值型別:

auto Func(); // 返回型別由編譯器推斷

#09 帶預設成員初始化器的聚合類

C++11 增加了預設成員初始化器,如果建構函式沒有初始化某個成員,並且這個成員擁有預設成員初始化器,就會⽤預設成員初始化器來初始化成員。

而在 C++11 中,聚合類(aggregate type)的定義被改為「明確排除任何含有預設成員初始化器」的型別。

因此,在 C++11 中,如果⼀個類含有預設成員初始化器,就不允許使⽤聚合初始化。C++14放鬆了這⼀限制:

struct CXX14_aggregate {
  int x;
  int y = 42;  // 帶有預設成員初始化器
};

// C++11 中不允許
// 但 C++14允許 且 a.y 將被初始化為42
CXX14_aggregate a = { 1 }; 

#10 decltype(auto)

允許 auto 的型別宣告使⽤ decltype 的規則。也即,允許不必顯式指定作為decltype引數的表示式,而使用decltype對於給定表示式的推斷規則。
—— From Wikipedia C++14

看一個例子:

// 在另一個函式中對下面兩個函式進行轉發呼叫
std::string  lookup1();
std::string& lookup2();

// 在 C++11 中,需要這麼實現
std::string look_up_a_string_1() {
    returnlookup1();
}
std::string& look_up_a_string_2() {
    returnlookup2();
}

// 在 C++14 中,可以通過 decltype(auto) 實現
decltype(auto) look_up_a_string_1() {
    return lookup1();
}
decltype(auto) look_up_a_string_2() {
    return lookup2();
}

C++17 新特性

#01 摺疊表示式

上文介紹了 C++11 中介紹了「變長引數模板」(C++11 第 #16 條)。在 C++11 中對變長引數進行展開比較麻煩,通常採用遞迴函式的方式進行展開:

void print() {  // 遞迴終止函式
   cout << "last" << endl;
}

template <class T, class ...Args>
void print(T head, Args... rest) {
   cout << "parameter " << head << endl;
   print(rest...); // 遞迴展開 rest 變長引數
}

C++17 引入「摺疊表示式」來進一步支援變長引數的展開:

// ⼀元左摺疊
// 只有一個操作符 「-」,且展開符 ... 位於引數包 args 的左側,固為一元左摺疊
template<typename... Args>
auto sub_val_left(Args&&... args) {
  return (... - args);
}

auto t = sub_val_left(2, 3, 4);   // ((2 - 3) - 4) = -5;

// 一元右摺疊
// 只有一個操作符 「-」,且展開符 ... 位於引數包 args 的右側,固為一元右摺疊
template<typename... Args>
auto sub_val_right(Args&&... args) {
  return (args - ...);
}
auto t = sub_val_right(2, 3, 4);  // (2 - (3 - 4)) = 3;

// 二元左摺疊
// 左右有兩個操作符 ,且展開符 ... 位於引數包 args 的左側,固為二元左摺疊
template<typename... Args>
auto sub_one_left(Args&&... args) {
  return (1 - ... - args);
}
auto t  = sub_one_left(2, 3, 4);  // ((1 - 2) - 3) - 4 = -8

// 二元右摺疊
// 左右有兩個操作符,且展開符 ... 位於引數包 args 的右側,固為二元右摺疊
template<typename... Args>
auto sub_one_right(Args&&... args) {
  return (args - ... - 1);        
}
auto t  = sub_one_right(2, 3, 4); //  2 - (3 - (4 - 1)) = 2

#02 類模板實參推導

C++17 之前類模板⽆法進⾏引數推導:

std::pair<int, string> a{ 1, "a"s }; // 需要指明 int, string 型別

C++17 實現了類模板的實參型別推導:

std::pair a{ 1, "a"s }; // C++17,類模板可自行推導實參型別

#03 auto 佔位的⾮型別模板形參

template<auto n> struct B { /* ... */ }

B<5> b1;    // OK: 非型別模板形參型別為 int
B<'a'> b2;  // OK: 非型別模板形參型別為 char
B<2.5> b3;  // 錯誤(C++20前):非型別模板形參型別不能是 double

#04 編譯期的 constexpr if 語句

C++17 將 constexpr 這個關鍵字引⼊到 if 語句中,允許在程式碼中宣告常量表達式的判斷條件

template<typename T>
auto print_info(const T& t) {
  if constexpr (std::is_integral<T>::value) {
    return t + 1;
  } else {
    return t + 1.1;
  }
}

上述程式碼將在編譯期進行 if 語句的判斷,從而在編譯期選定其中一條分支。

#05 內聯變數(inline 變數)

看一個例子:

// student.h
extern int age;  // 全域性變數

struct  Student {
   static int age;  // 靜態成員變數
};

// student.cpp
int age = 18;
int Student::foo = 18;

在 C++17 之前,如果想要使用全域性變數或類的靜態成員變數,需要在標頭檔案中宣告,然後在每個 cpp 檔案中定義。

C++17 支援宣告內聯變數達到相同的效果:

// student.h
inline int age = 18;

struct Student {
   static inline int age = 18;
};

#06 結構化繫結

類似於 JavaScript 中的解構賦值

⽰例:

tuple<int, double, string> f() {
  return make_tuple(1, 2.3, "456");
}

int main() {
  int arr[2] = {1,2};
  // 建立 e[2]
  // 複製 arr 到 e, 然後 a1 指代 e[0], b1 指代 e[1]
  auto [a1, b1] = arr;
  cout << a1 << ", " << b1 << endl;

  // a2 指代 arr[0], b2 指代 arr[1]
  auto& [a2, b2] = arr;
  cout << a2 << "," << b2<< endl;

  // 結構化繫結 tuple
  auto [x, y, z] = f();
  cout << x << ", " << y << ", " << z << endl;

  return 0;
}

#07 if/switch 語句的變數初始化

if/switch 語句宣告並初始化變數,形式為:if (init; condition) 和 switch (init; condition)。例⼦:

for (int i = 0; i < 10; i++) {
  // int count = 5; 這條初始化語句直接寫在 if 語句中
  if (int count = 5; i > count) {
    cout << i << endl;
  }
}

// char c(getchar()); 這條初始化語句直接寫在 switch 語句中
switch (char c(getchar()); c) {
  case 'a': left(); break;
  case 'd': right(); break;
  default: break;
}

#08 u8-char

字元字首:

u8'c-字元' // UTF-8 字元字面量

注意和上文的「字串字首」相區分,C++11 引入的 u8 是字串字首,C++17 補充 u8 可作為字元的字首。

#09 簡化的巢狀名稱空間

namespace X { namespace Y { … }}  // 傳統
namespace X::Y { … }              // C++17 簡化名稱空間

#10 using 宣告語句可以宣告多個名稱

struct A {
    void f(int) {cout << "A::f(int)" << endl;}
};
struct B {
    void f(double) {cout << "B::f(double)" << endl;}
};
struct S : A, B {
    using A::f, B::f; // C++17
};

#11 將 noexcept 作為型別系統的一部分

與返回型別相似,異常說明成為函式型別的一部分,但不是函式簽名的一部分

// 下面函式是不同型別函式,但擁有相同的函式簽名
void g() noexcept(false);
void g() noexcept(true);

#12 新的求值順序規則

在 C++17 之前,為了滿足各個編譯器在不同平臺上做相應的優化,C++ 對一些求值順序未做嚴格規定。最典型的例子如下:

cout << i << i++;  // C++17 之前,未定義行為
a[i] = i++;              // C++17 之前,未定義行為
f(++i, ++i);           // C++17 之前,未定義行為

具體的,C++17 規定了以下求值順序:

  • a.b
  • a->b
  • a->*b
  • a(b1, b2, b3)
  • b @= a
  • a[b]
  • a << b
  • a >> b

順序規則為:a 的求值和所有副作用先序於 b,但同一個字母的順序不定

#13 強制的複製消除(guaranteed copy elision)

C++17 引入「強制的複製消除」,以便在滿足一定條件下能夠確保消除物件的複製。

在 C++11 之前已經存在所謂的複製消除技術(copy elision),即編譯器的返回值優化 RVO/NRVO。

RVO(return value optimization): 返回值優化
NRVO(named return value optimization):具名返回值優化

看下面的例子:

T Func() {
  return T();
}

在傳統的複製消除(copy elision)規則下,上述程式碼將會產生一個臨時物件,並將其拷貝給「返回值」。這個過程可能會被優化掉,也就是拷貝/移動函式根本不會被呼叫。但程式還是必須提供相應的拷貝函式。

再看如下程式碼:

T t = Func();

上述程式碼會將返回值拷貝給 t,這個拷貝操作依然可能被優化掉,但同樣的,程式依然需要提供相應的拷貝函式。

從上文可知,在傳統的複製消除規則下,下面程式碼是非法的:

// 傳統的複製消除即使優化了拷貝函式的呼叫
// 但還是會檢查是否定義了拷貝函式等
struct T {
    T() noexcept = default;
    T(const T&) = delete; // C++11 中如果不提供相應的拷貝函式將會導致 return 與 賦值錯誤
    T(T&&) = delete;
};

T Func() {
  return T();
}

int main() {
  T t = Func();
}

而「強制複製消除」對於純右值 prvalue[5],將會真正消除上述複製過程[6],也不會檢查是否提供了拷貝/移動函式,所以上述程式碼在 C++17 中是合法的。

[5] 在 C++17 之前,純右值為臨時物件,而 C++17 對純右值 prvalue 的定義進行了擴充套件:能夠產生臨時物件但還未產生臨時物件的表示式,如上例程式碼中的 Func();
[6] 消除的原理:在滿足「純右值賦值給泛左值」這個條件時,T t = Func(); 會被優化成類似於 T t = T(); 這中間不會產生臨時物件。

但另一方面,對於「具名臨時物件」,不會進行「強制複製消除」:

T Func() {
   T t = ...;
   ...
   return t;
}

T 還是必須提供拷貝/移動函式,所以 C++17 對於具名返回值優化 NRVO (named return value optimization) 沒有變化。

關於強制複製消除,可以參考下面連結的第一個回答,回答的很清楚:
How does guaranteed copy elision work?

這一切是否來源於 C++ 的初始設計問題: = 運算子的預設過載,賦予了 = 運算子物件拷貝的語義。

#14 lambda 表示式捕獲 *this

#include <iostream>
 
struct Baz {
  auto foo() {
    // 通過 this 捕獲物件,之後在 lambda 即可訪問物件的成員變數 s
    return[this]{ std::cout << s << std::endl; };
  }
 
  std::string s;
};
 
int main() {
  auto f1 = Baz{ "ala" }.foo();
  auto f2 = Baz{ "ula" }.foo();
  f1();
  f2();
}

但上述程式碼存在一個缺陷:捕獲的是當前物件,如果 lambda 表示式對成員變數的訪問超出了當前物件的生命週期,就會導致問題。

C++17 提供了 *this 捕獲當前物件的副本

auto foo() {
  return[*this]{ std::cout << s << std::endl; };
}

#15 constexpr 的 lambda 表示式

C++17 的 lambda 宣告為 constexpr 型別,這樣的 lambda 表示式可以用在其他需要 constexpr 型別的上下文中。

int y = 32;

auto func = [y]() constexpr {
  int x = 10;

  return y + x;
};

#16 屬性名稱空間不必重複

在上文的 C++11 #21 條中已經介紹了屬性的概念,對於由實現定義的行為的非標準屬性,可能會帶有名稱空間:

[[gnu::always_inline, gnu::const, gnu::hot, nodiscard]]
inline int f(); // 宣告 f 帶四個屬性

[[gnu::always_inline, gnu::const, gnu::hot, nodiscard]]
int f(); // 同上,但使用含有四個屬性的單個屬性說明符

C++11 中上述屬性的名稱空間需要重複宣告,C++17 簡化了屬性名稱空間的定義:

[[using gnu : const, always_inline, hot]] [[nodiscard]]
int f[[gnu::always_inline]](); // 屬性可出現於多個說明符中

#17 新屬性 [[fallthrough]] [[nodiscard]] 和 [[maybe_unused]]

C++11 僅自帶了兩個標準屬性,C++17 繼續擴充套件了幾個標準屬性。

fallthrough

// 以下程式碼因為沒有 case 中沒有 break;
// 所以將會發生 case 穿透
// 編譯時編譯器將會發出警告
int x = 2;
switch (x) {
  case 2:
    result++;
  case 0:
    result++;
  default:
    result++;
}

// 有時候我們需要 case 穿透,如匹配到 2 就一直執行後續的 case
// 此時可以使用屬性 [[fallthrough]],使用後,編譯器將不會發出警告
switch (x) {
    case 2:
      result++;
      [[fallthrough]];  // Added
    case 0:
      result++;
      [[fallthrough]];  // Added
    default:
      result++;
  }

nodiscard
在開發過程中經常需要對函式返回值進行檢查,這一步驟在不少業務場景下是必須的,例如:

// 許多人會遺漏對返回值進行檢查的步驟
// 導致了很多業務層面潛在的缺陷
if (CallService() != ret) {
  // ... 
}

// C++17 引入 [[nodiscard]] 屬性來「提醒」呼叫者檢查函式的返回值
[[nodiscard]] int CallService() {
  return CallServiceRemote();
}

CallService();              // 如果只調用而不檢查,編譯器將發出警告
if (CallService() != ret) { // pass
  // ...
}

maybe_unused
如果我們以 -Wunused 與 -Wunused-parameter 編譯以下程式碼,編譯器則可能報出警告:

int test(int a, int b, int c) {
  int result = a + b;

#ifdef ENABLE_FEATURE_C
  result += c;
#endif
  return result;
}

原因是編譯器認為 c 是未用到的變數,但實際上並非無用。C++17 中可以使用 [[maybe_unused]] 來抑制「針對未使用實體」的警告:

int test(int a, int b, [[maybe_unused]] int c) {
  int result = a + b;

#ifdef ENABLE_FEATURE_C
  result += c;
#endif
  return result;
}

#18 __has_include

表明指定名稱的頭或原始檔是否存在:

#if __has_include("has_include.h")
  #define NUM 1
#else
  #define NUM 0   
#endif

C++20 新特性

#01 特性測試巨集

為 C++11 和其後所引入的 C++ 語言和程式庫的功能特性定義了一組前處理器巨集。使之成為檢測這些功能特性是否存在的一種簡單且可移植的方式。例如:

__has_cpp_attribute(fallthrough)     // 判斷是否支援 fallthrough 屬性
#ifdef __cpp_binary_literals              // 檢查「二進位制字面量」特性是否存在 
#ifdef __cpp_char8_t                          // char8 t
#ifdef __cpp_coroutines                     // 協程
// ...

#02 三路比較運算子 <=>

// 若 lhs < rhs 則 (a <=> b) < 0
// 若 lhs > rhs 則 (a <=> b) > 0
// 而若 lhs 和 rhs 相等/等價則 (a <=> b) == 0

lhs <=> rhs

#04 範圍 for 中的初始化語句和初始化器

C++17 引入了 if/switch 的初始化語句,C++20 引入了範圍 for 的初始化:

// 將 auto list = getList(); 初始化語句直接放在了範圍 for 語句中
for (auto list = getList(); auto& ele : list) {
    // ele = ....
}

另外 C++20 的範圍 for 還可支援一定的函數語言程式設計風格,例如引入管道符 | 實現函式組合:

// 範圍庫
auto even = [](int i){ return 0 == i % 2; };
auto square = [](int i) { return i * i; };
// ints 輸出到 std::view::filter(even) ,處理後得到所有偶數
// 上一個結果輸出到 std::view::transform(square),將所有偶數求平方
// 迴圈遍歷所有偶數的平方
for (int i : ints | std::view::filter(even) | 
                      std::view::transform(square)) {
 // ...
}

#05 char8_t

C++20 新增加 char8_t 型別。

char8_t 用來表示 UTF-8 字元,要求大到足以表示任何 UTF-8 編碼單元( 8 位)。

#06 [[no_unique_address]]

[[no_unique_address]] 屬性修飾的資料成員可以被優化為不佔空間:

struct Empty {}; // 空類
struct X {
  int i;
  Empty e;
};
struct Y {
  int i;
  [[no_unique_address]] Empty e;
};
struct Z {
  char c;
  [[no_unique_address]] Empty e1, e2;
};
struct W {
  char c[2];
  [[no_unique_address]] Empty e1, e2;
};

int main() {
  // 任何空類型別物件的大小至少為 1
  static_assert(sizeof(Empty) >= 1);

  // 至少需要多一個位元組以給 e 唯一地址
  static_assert(sizeof(X) >= sizeof(int) + 1);

  // 優化掉空成員
  std::cout << "sizeof(Y) == sizeof(int) is " << std::boolalpha << (sizeof(Y) == sizeof(int)) << '\n';

  // e1 與 e2 不能共享同一地址,因為它們擁有相同型別,儘管它們標記有 [[no_unique_address]]。
  // 然而,其中一者可以與 c 共享地址。
  static_assert(sizeof(Z) >= 2);

  // e1 與 e2 不能擁有同一地址,但它們之一能與 c[0] 共享,而另一者與 c[1] 共享
  std::cout << "sizeof(W) == 2 is " << (sizeof(W) == 2) << '\n';
}

#07 [[likely]]

[[likely]] 屬性用來告訴編譯器哪條分支執行的概率會更大,從而幫助編譯器進行程式碼編譯的優化

if (a > b) [[likely]] {
  // ...
}

第一直覺真的是奇葩特性,好奇能優化到什麼程度以至於專門增加語言特性來要求程式設計師配合這種優化
包括下文的標頭檔案,讓我覺得 C++ 很多時候不是編譯器為程式設計師服務,而是程式設計師為編譯器服務

#08 [[unlikely]]

與 [[likely]] 相對應:

if (a>b) [[unlikely]] {
  // ...
}

#09 lambda 初始化捕獲中的包展開

在 C++20 之前,lambda 表示式對與包展開無法進行初始化捕獲,如果想要對包展開進行初始化捕獲,需要通過 make_tuple 和 apply 來實現,如下所示:

template <class... Args>
auto delay_invoke_foo(Args... args) {
    // 對 args 進行 make_tuple,然後再用 apply 恢復
    return [tup=std::make_tuple(std::move(args)...)]() -> decltype(auto) {
        return std::apply([](auto const&... args) -> decltype(auto) {
            return foo(args...);
        }, tup);
    };
}

C++20 將直接支援 lambda 對包展開進行初始化捕獲,如下所示:

template <class... Args>
auto delay_invoke_foo(Args... args) {
    // 直接 ...args = xxxxx
    return [...args=std::move(args)]() -> decltype(auto) {
        return foo(args...);
        
    };
}

#10 移除了在多種上下文語境中,使用 typename 關鍵字以消除型別歧義的要求

P0634R3
C++20 之前,在使用了模板型別的地方需要使用 typename 來消除歧義,如下所示:

template<typename T>
typename std::vector<T>::iterator // std::vector<T>::iterator 之前必須使用 typename 關鍵字

C++20 則允許在一些上下文語境中省略 typename,如下所示:

template<typename T>
std::vector<T>::iterator // 省略 typename 關鍵字

#11 consteval、constinit

consteval
上文提及過 constexpr 函式可以在編譯期執行,也可以在執行期執行。C++20 為了更加明確場景和語義,提供了只能在編譯期執行的 consteval,consteval 修飾的函式返回的值如果不能在編譯器確定,則編譯無法通過。

constinit
在 C++ 中,對於靜態儲存期的變數的初始化,通常會有兩種情況:

  • 在編譯期初始化
  • 在被第一次載入宣告時初始化

其中第二種情況由於靜態變數初始化順序的原因存在著隱藏的風險。

所以 C++20 提供了 constinit,以便使某些應該在編譯期初始化的變數被確保的在編譯期初始化。

#12 更為寬鬆的 constexpr 要求

從 C++11 一直到 C++20 就一直在給 constexpr 「打補丁」,就不能一次性擴充套件其能力嗎

引用自 C++20 新增特性
C++20 中 constexpr 擴充套件的能力:

  • constexpr虛擬函式
    • constexpr 的虛擬函式可以重寫非 constexpr 的虛擬函式
    • 非 constexpr 虛擬函式將過載 constexpr 的虛擬函式
  • constexpr 函式支援:
    • 使用 dynamic_cast() 和 typeid
    • 動態記憶體分配
    • 更改union成員的值
    • 包含 try/catch
      • 但是不允許 throw 語句
      • 在觸發常量求值的時候 try/catch 不發生作用
      • 需要開啟 constexpr std::vector
  • constexpr 支援 string & vector 型別

#13 規定有符號整數以補碼實現

在 C++20 之前,有符號整數的實現沒有明確以標準的形式規定(雖然在實現時基本都採用補碼)。C++20 明確規定了有符號整數使用補碼實現。

#14 使用圓括號的聚合初始化

C++20 引入了一些新的聚合初始化形式,如下所示:

T object = { .designator = arg1 , .designator { arg2 } ... };  //(since C++20)
T object { .designator = arg1 , .designator { arg2 } ... };     // (since C++20)
T object (arg1, arg2, ...);                                                           // (since C++20)

其中之前沒有過的就是第三種形式: T object (arg1, arg2, ...),使用圓括號進行初始化。

#15 協程

程序:作業系統資源分配的基本單元。排程涉及到使用者空間和核心空間的切換,資源消耗較大。
執行緒:作業系統執行的基本單元。在同一個程序資源的框架下,實現搶佔式多工,相對程序,降低了執行單元切換的資源消耗。
協程:和執行緒非常類似。但是轉變一個思路實現協作式多工,由使用者來實現協作式排程(主動交出控制權)

高德納 Donald Knuth:

子程式就是協程的一種特例

協程是廣義的函式(子程式),只是它的流程由使用者進行一定程度的函式過程切換和控制

舉一個例子:

# 協程實現的生產者和消費者
def consumer():
  r = ''
  while True:
    n = yield r
    if not n:
      return
    print('[CONSUMER] Consuming %s...' % n)
    time.sleep(1)
    r = '200 OK'

def produce(c):
  c.next()
  n = 0
  while n < 5:
    n = n + 1
    print('[PRODUCER] Producing %s...' % n)
    r = c.send(n)
    print('[PRODUCER] Consumer return: %s' % r)
  c.close()

if __name__=='__main__':
  c = consumer()
  produce(c)

生產者生產訊息,待消費者執行完畢後,通過 yield 讓出控制權切換回生產者繼續生產。

yield: 執行到這裡主動讓出控制權,返回一個值,並等待上一個上下文對自己的進一步排程

上面是協程的的純粹概念,但是很多語言對協程會有不同的實現和封裝,導致協程的概念被進一步擴充套件和延伸。

例如 golang 中的 Goroutines 其實並不是一個純粹的協程概念,而是對協程和執行緒的封裝和實現,可以說在使用者狀態下的執行單元排程,同時又解決了傳統協程無法利用多核能力的缺陷。所以很多資料將其稱為 「輕量級執行緒」或 「使用者態執行緒」。

另外,在非同步程式設計方面,協程有一個特別的優勢:
通過更符合人類直覺的順序執行來表達非同步邏輯

在 JS 生態中(尤其以 Node.js 為代表)我們編寫非同步邏輯,經常使用回撥來實現結果返回。而如果是多層級非同步呼叫的場景,容易陷入 「callback hell 回撥地獄」。

如下所示:

fs.readFile(fileA, function (err, data) {
  fs.readFile(fileB, function (err, data) {
    // ...
  });
});

JS 後續引入了 Promise,簡化回撥呼叫形式,如下所示:

readFile(fileA)
.then(function(data){
  console.log(data.toString());
})
.then(function(){
  return readFile(fileB);
})
.then(function(data){
  console.log(data.toString());
})
.catch(function(err) {
  console.log(err);
});

再後續引入了協程的一種實現——Generator 生成器

var fetch = require('node-fetch');

function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

var g = gen();
var result = g.next();

result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});

Generator 函式可以暫停執行(yield)和恢復執行(next),這是它能用來實現非同步程式設計的根本原因

而 JS 後續底層通過 yield/generator 實現的 async & await 非同步程式設計體驗,也會使得 JS 程式設計師對協程的直觀感受為「回撥排程器」。

而 C++20 引入的則是相對純粹的協程,例如可以實現一個 generator函式或者生成器:

experimental::generator<int> GetSequenceGenerator( 
    int startValue, 
    size_t numberOfValues) { 
    for (int i = 0 startValue; i < startValue + numberOfValues; ++i){ 
        time_t t = system_clock::to_time_t(system_clock::now()); 
        cout << std:: ctime(&t); co_yield i; 
    } 
} 
int main() {
    auto gen = GetSequenceGenerator(10, 5); 
    for (const auto& value : gen) { 
        cout << value << "(Press enter for next value)" << endl; 
        cin.ignore(); 
    } 
}

#16 模組

歷史包袱-標頭檔案

請看如下程式碼:

// person.cpp
int rest() {
  Play();
  return 0;
}

// game.cpp
int play() {
  LaunchSteam();
  return 0;
}
  1. 由於 C/C++ 時代 .obj 等結果檔案可能來自於其他語言。固每個原始檔不與其他原始檔產生關聯,需獨立編譯。在這樣的背景下,我們站在編譯器的角度嘗試編譯 person.cpp ,會發現編譯將無法進行。原因是 Play 的返回型別、引數型別等元資訊無法獲取。那麼是否可以生成外部符號等待連結階段呢?
  2. 答案是否定的。即無法推遲到連結階段。原因是 C++ 編譯時不會將函式的返回值、引數等元資訊編譯進 .obj 等結果,固在連結階段依然獲取不到 Play 函式相關的元資訊。之所以沒有像 Java/C# 等現代語言這樣將元資訊寫到編譯結果中,是因為 C/C++ 時代記憶體等資源稀缺,所以想方設法的節省各種資源。

而由於上述歷史原因,導致了 C++ 最終將這種不便轉交給了程式設計師。程式設計師在呼叫另一個原始檔的函式時需要事先宣告函式原型,而如果在每個使用到相應函式的原始檔中都重複宣告一次就太過於低階,於是出現了所謂的標頭檔案,簡化宣告工作。

另一方面,標頭檔案從一定程度起到了介面描述的作用,但有些人把標頭檔案當作是「實現與介面分離的設計思想」下的成果就非常的牽強了。

標頭檔案本質上是圍繞著編譯期的一種概念,是 C/C++ 由於歷史原因不得不由程式設計師使用標頭檔案輔助編譯器完成編譯工作。

而介面的概念是圍繞著業務開發或程式設計階段的,是另一層面的事情。

如果不好理解,可以思考一下,Java/C# 沒有標頭檔案的語言是如何實現所謂「標頭檔案提供介面」這一功能的?

如果需要實現,編譯器可以直接從原始碼檔案抽離出介面資訊生成介面檔案即可,而且還可以根據訪問許可權來決定哪些該對外暴露,哪些不能暴露。甚至可以以 .h 為字尾讓那些覺得「標頭檔案起到介面作用」的程式設計師好受些。

C++20 引入了模組,模組的其中一個作用就是將 header編譯單元統一在了一起。

// example 模組
export module example; //宣告一個模組名字為example
export int add(int first, int second) { //可以匯出的函式
  return first + second;
}

// 使用 example 模組
import example; //匯入上述定義的模組
int main() {
  add(1, 2); //呼叫example模組中的函式
}

#17 限定與概念(concepts)

concepts 是 C++20 的重要更新之一,它是模板能力的擴充套件。在 C++20 之前,我們的模板引數是沒有明確限定的,如下所示:

template<class L, class T>
void find(const L& list, const T& t); // 從 list 列表中查詢 t

上面的引數型別 L 與 T 沒有任何的限制,但實際上是存在著隱含的限定條件的:

  • L 應該是一個可迭代型別
  • L 中的元素型別應該和 T 型別相同
  • L 中的元素應該和 T 型別可進行相等比較

程式設計師應當知曉上述隱含條件,否則編譯器就會輸出一堆錯誤。而現在可以通過 concepts 將上述限定條件告知編譯器,在使用錯誤將得到直觀的錯誤原因。

例如使用 concepts 限定引數可 hash:

// 定義概念
template<typename T>
concept Hashable = requires(T a) {
  // 下面語句的限定含義為:
  // 限定  std::hash(a) 返回值可轉換成 std::size_t
  { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};

// 使用概念對模板引數進行限定
template<typename T>
auto my_hash(T) requires Hashable<T> {
  // ....
}

對於上述的 my_hash 函式也可通過簡化的方式進行:

// 簡化
template<Hashable T>
auto my_hash(T) {
  // ....
}

#18 縮略函式模板

通常宣告函式模板的形式如下:

template<class T> void f(T);
template<C1 T> void f2(T); // C1 如果是一個 concept 概念
// ...

C++20 可以採用 autoconcept auto 來實現更為簡短的函式模板宣告形式:

void f1(auto);       // 等同於 template<class T> void f(T);
void f2(C1 auto); // template<C1 T> void f2(T);
// ...

#19 陣列長度推導

C++20 將允許 new int[]{1, 2, 3} 的寫法,編譯器可自動推導陣列長度。

小結

C++ 最根本的設計理念就是為了執行效率服務,甚至專門增加新特性要求程式設計師配合編譯器來做優化。但另一方面, C++ 後期一直從 Java/JavaScript/Go/Python 等語言中借鑑特性,而其中很多是無關緊要的語法糖,對於真正至關重要的特性卻又一直拖到了 0202 年才推出標準。

C++ 20 真正在業界扎穩又是要到何年何月,至於形成與其他現代語言一樣完善、統一的生態更是遙不可期

這導致本就繁雜的 C++ 的語法隨著時間推移變得更加混亂,這進一步提高了 C++ 的學習與使用成本。唯一的好處就是提高了部分現有 C++ 程式設計師的自豪感,畢竟部分程式設計師是以自己掌握的工具難度為傲的。這些人不僅將「工具的難度」與「技術水平」掛鉤,有時甚至以此標榜自己的智商。建議有此想法的人閱讀並背誦新華字典全典或者用匯編完成所有工作

C++ 有其對應的應用場景,在一些執行效率要求極高的基礎元件的開發上,在絕大多數的遊戲開發場景下,C++ 有其不可替代性。但在一些上層的應用場景,尤其是在更接近使用者的網際網路業務上使用 C++ 基本都是由於歷史債務[7]



作者:404_89_117_101
連結:https://www.jianshu.com/p/8c4952e9edec
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。