小山的 C++ 快速上手筆記

Deploy

本書是為了讓不熟悉 C++ 的人,在有接觸過其他 OOP 程式語言(例如 Java)的前提下快速上手所做的筆記。

先備知識

預期閱讀此筆記前,假定讀者已經:

  • 學習過 C 語言(或類似語法的語言)
  • 熟悉其他物件導向 (OOP) 程式語言 (e.g., Java, JavaScript, Golang...)

內容哪裡來的?

這本筆記的內容主要是我從以下網站中整理出來的重點回顧:

C++ 基礎知識加強

加強一些 C++ 與其他語言不同之處。

特別資料型別

  • bool : boolean 型別
  • wchar_t : wide characters (16 bits) 字元,用來補足原本的 char 型別大小不足的問題,以支援 Unicode
  • 固定大小整數型別,避免了傳統 C 語言的 int 等型別大小不固定的問題,例如:

取得型別的 Max & Min

  • 方法一: 使用內建常數
    • INT32_MAX (需要引入 stdint.h)
  • 方法二: std::numeric_limits<T>::min()
    • T 可以換成任意內建型別

變數宣告修飾子

  • Storage Class
    • auto : 跟代表自動推導的 auto 不同,限制變數只能被宣告的 scope 內的程式碼存取,預設情況下 local variable 前都會自動加上 auto
      • Example: auto int a = 1
    • register : 提示 compiler 將變數存在 CPU register (C++11 之後 deprecated)
    • static :
      • 用在 local variable 會讓這個 local variable 的值與記憶體空間一直保留到程式結束
      • 用在 global variable 會限制該 variable 的 scope 在所在的 file 中
      • 用在 class member 會讓該 member shared 在所有 object 中
    • extern : 代表變數定義在其他檔案內,linking 時才會引入
    • mutable : 只能用在 class member 上,當物件的變數被指定為 const 時,該 class member 仍能夠被外界修改。
  • 全域變數宣告時會自動初始化為 NULL / 0

Type Qualifiers

  • const : 常數
  • volatile : 防止存取此變數的動作因為 compiler optimization 的關係被調換位置
    • 同時也限制 CPU 不能快取此變數在 CPU cache (multi-threading 時很重要)
  • restrict : 限制只有此指標變數可以指向該記憶體位置

Reference (參考)

Reference 是 C++ 引入給變數設定「別名」的功能,跟 pointer 關鍵差異如下:

  • Null 值:Pointer 允許 NULL,reference 不行
  • 修改指向位置:Pointer 的變數可以途中修改成指向其他記憶體位置,reference 不行
  • 初始化:Reference 初始化時一定要設定指向的變數,pointer 沒有限制

宣告方式:

// declare simple variables
int    i;
double d;

// declare reference variables
int&    r = i;
double& s = d;

Reference 可以從 function 被 return:

double& setValues( int i ) {
   return vals[i];   // return a reference to the ith element
}

但要注意不能 return local variable 的 reference

Arrays

  • 宣告方式: double a[10];

  • 多維陣列: int threedim[5][10][4];

  • 初始化: double balance[] = {1000.0, 2.0, 3.4, 17.0, 50.0};

  • 初始化二維陣列:

    int a[3][4] = {
       {0, 1, 2, 3} ,   /*  initializers for row indexed by 0 */
       {4, 5, 6, 7} ,   /*  initializers for row indexed by 1 */
       {8, 9, 10, 11}   /*  initializers for row indexed by 2 */
    };
    
  • Array 的變數其實就是 pointer:

    double *p;
    double balance[10];
    
    p = balance;
    
    • balance[4] 等價於 *(balance + 4)
  • Array 作為 paramater 傳進 function 方法有兩種常見方式:

    • Method 1: 用 pointer

      void myFunction(int *param) {
         // ...
      }
      
    • Method 2: 用 unsized array

      void myFunction(int param[]) {
         // ...
      }
      
  • 從 function return array 只有一種做法,就是回傳 pointer:

    int * myFunction() {
       // ...
    }
    

C++ String

C++ 有內建 String 型別,所以可以不使用 C-style string (也就是字元陣列)

使用方式:

#include <string>
using std::string;

string s = "aabbcc";
s.size(); // return 6
s.find("bb"); // return 2
s += "123"; // s = aabbcc123

字面值直接使用 string 型別(省掉一個轉換步驟,需要 using namespace std::literals):

using namespace std::literals;
string s3_2 = "hello world"s;

基本操作:

os << s // 輸出 s 
is >> s // 輸入 s
s.empty() // 檢查 s 是否為空
s.size() // s 目前長度
s[n] // 直接取得 s 的第 n 個元素
s.at(n) // 同樣取得第 n 個元素,但會檢查邊界
s1 + s2 // 把 s1 加 s2 取得新的字串
s1.append(s2) // 把 s2 加到 s1 後面,不會產生新字串
s1 += s2 // 等價於 s1.append(s2)
s1 = s2 // 拷貝複製 s2
s1 != s2 // 比較 s1 和 s2 是否相同
<, <=, ==, >=, > // 做大小比較,以字典排序

完整 C++ String 教學: https://tigercosmos.xyz/post/2023/06/c++/std-string-beginner/

Standard Input/Output

C++ 有以下幾個常用的 input/output streams

  • cin : 標準輸入
  • cout : 標準輸出
  • cerr : 標準錯誤(但沒有 buffer)
  • clog : 標準錯誤(但有 buffer)

C++ STL

Standard Template Library (STL) 是 C++ 之中最重要的函式庫,包含多項重要的元件:

  • Containers: 各種儲存與操作資料的容器
  • Algorithms: 各種高效的演算法
  • Iterators: 用來依照某些邏輯存取容器內資料的存取器
  • Function Objects: 提供將 function 做為參數傳入 function 的方法,可以幫助客製化演算法的行為
  • Adapters: 可以用來修改 STL 內其他元件行為的工具

Containers

STL container 清單: https://cplusplus.com/reference/stl/

需要注意的 containers:

  • forward_list : 只有向前指針的 linked list,比 list (具備前後指針) 使用更少的空間

  • priority_queue : 用 heap 實作的 queue,最大的東西會在最前面

    • 一般用法:

      int arr[6] = { 10, 2, 4, 8, 6, 9 };
      priority_queue<int> pq;
       
      // pushing array sequentially one by one
      for (int i = 0; i < 6; i++) {
          pq.push(arr[i]);
      }
       
      // printing priority queue
      cout << "Priority Queue: ";
      while (!pq.empty()) {
          cout << pq.top() << ' ';
          pq.pop();
      }
      
    • 修改成最小在最前面:

      int arr[6] = { 10, 2, 4, 8, 6, 9 };
      priority_queue<int, vector<int>, greater<int>> gquiz(arr, arr + 6);
      
    • 使用自製的 comparator:

      class Foo { /* data */ };
      
      class Comparator
      {
      public:
          bool operator() (Foo, Foo)
          {
      		// Should the first one be in front of the second one?
              return true;
          }
      };
      
      int main()
      {
          std::priority_queue<Foo, std::vector<Foo>, Comparator> pq;
          return 0;
      }
      
  • set : sorted set,用紅黑樹實作

  • map : sorted map,實作與 set 相同

  • multiset : 跟 set 一樣是 sorted set,但可以存重複資料

  • multimap : 跟 map 一樣是 sorted map,但可以存重複的 key

    • 注意用 find(key) 只會拿到一個 key-value pair,如果想要抓出所有重複 key 的 pair,要使用 equal_range(key)
      • equal_range(key) 回傳的是一個 pair, first 是 iterator 的開頭(lower bound, inclusive), second 是 iterator 的結尾 (upper bound, exclusive)

Algorithms

常用 algorithms:

  • sort(begin, end) : 使用 IntroSort 進行排序

    • IntroSort:先嘗試使用 quick sort。 如果發現 partition 差太多,改成用 heap sort。 如果 size 夠小,改成用 insertion sort。

    • 基本用法:

      int arr[] = {3, 5, 1, 2, 4};
      sort(arr, arr + 5);
      
      vector<int> vec {3, 5, 1, 2, 4};
      sort(vec.begin(), vec.end());
      
  • binary_search(begin, end, value) : 使用 binary search

  • reverse(begin, end) : 反轉順序

Iterator

可以看 C++ Reference 官網的介紹,簡單明瞭: https://cplusplus.com/reference/iterator/

只要搞懂 Input, Output, Forward, Bidirectional, Random Access 這五種 iterator 的差異,跟他們的從屬關係就好。

Function Objects (Functors)

直接看一例子,這個例子是寫一個 class 幫所有 array 所有的 element 加上想要的數字:

#include <bits/stdc++.h>
using namespace std;

// A Functor
class increment
{
private:
	int num;
public:
	increment(int n) : num(n) { }

	// This operator overloading enables calling
	// operator function () on objects of increment
	int operator () (int arr_num) const {
		return num + arr_num;
	}
};

// Driver code
int main()
{
	int arr[] = {1, 2, 3, 4, 5};
	int n = sizeof(arr)/sizeof(arr[0]);
	int to_add = 5;

	transform(arr, arr+n, arr, increment(to_add));

	for (int i=0; i<n; i++)
		cout << arr[i] << " ";
}

主要是提供了改寫 () 行為的能力,所以讓一個 object 可以被像 function 一樣呼叫。

其他

Pairs

C++ STL 提供了 pair 可以使用:

// CPP program to illustrate Pair in STL
#include <iostream>
#include <utility>

using namespace std;

// Driver Code
int main()
{
	// defining a pair
	pair<int, char> PAIR1;

	// first part of the pair
	PAIR1.first = 100;
	// second part of the pair
	PAIR1.second = 'G';

	cout << PAIR1.first << " ";
	cout << PAIR1.second << endl;

	return 0;
}

  • Pair 可以被比較,會先比第一個再比第二個

C++ 物件導向

Class 的宣告與定義

基本例子:

#include <iostream>
 
using namespace std;
 
class Box {
   public:
      double length;
      void setWidth( double wid );
      double getWidth( void );
 
   private:
      double width;
};
 
// Member functions definitions
double Box::getWidth(void) {
   return width ;
}
 
void Box::setWidth( double wid ) {
   width = wid;
}
 
// Main function for the program
int main() {
   Box box;
 
   // set box length without member function
   box.length = 10.0; // OK: because length is public
   cout << "Length of box : " << box.length <<endl;
 
   // set box width without member function
   // box.width = 10.0; // Error: because width is private
   box.setWidth(10.0);  // Use member function to set it.
   cout << "Width of box : " << box.getWidth() <<endl;
 
   return 0;
}

Initialization List

在 constructor 上可以加上特殊的 list 初始化 member data,如下:

C::C(double a, double b, double c): X(a), Y(b), Z(c) {
   ....
}

Copy Constructor

C++ 規定每一個 class 都要有一個 copy constructor 具備可以複製整個物件的能力,形式如下:

classname (const classname &obj) {
   // body of constructor
}

如果沒有的話 C++ 會自動創建一個預設的,做法是會呼叫所有 member variable 的 copy constructor 實現 deep copy。

Friend Functions & Classes

Class 內可以定義 friend function 與 class 如下:

class Box {
   double width;
   
   public:
      double length;

      friend void printWidth( Box box );
      void setWidth( double wid );

      friend class ClassTwo;
};

這樣寫代表外面的 printWidthClassTwo 就可以直接存取 Box 的 private variable width

Static Members

C++ 的 class 可以定義 static member,但變數的初始化必須要寫在 class definition 外面:

// foo.h
class foo
{
    private:
        static int i;
};

// foo.cpp
int foo::i = 0;

其他特性基本跟 Java 的 static 相同。

繼承性

與 Java 的關鍵差異:

  • 可以繼承多個 class
    • 這點比較像是 Java 的 interface
  • 繼承時可以設定 access modifier
    • 例如下面的例子將 base class 設定為 public

      // Base class
      class Shape {
         // ...
      };
      
      // Derived class
      class Rectangle: public Shape {
         // ...
      };
      
    • 這個用途是可以修改原本 base class 內的 member 存取性質,但只能改得更嚴格,不能放得更鬆

      • 設定為 public ⇒ 不修改 base class 的 member
      • 設定為 protected ⇒ 原本是 public 的 member 都會變成 protected
      • 設定為 private ⇒ 原本是 public 與 protected 的 member 會變成 private

Operators Overloading

C++ 的一大特色就是可以修改 operator 對 class 造成的效果

假設有一個 Box class,希望將 + 的效果改成分別把長寬高相加,那要這樣修改:

class Box {
   public:
      // ...
      
      // Overload + operator to add two Box objects.
      Box operator+(const Box& b) {
         Box box;
         box.length = this->length + b.length;
         box.breadth = this->breadth + b.breadth;
         box.height = this->height + b.height;
         return box;
   // ...
}

Function Override & Virtual Functions

在 C++ 中,如果衍伸類別要改寫基底類別的 method,基底類別就必須要將 method 定義為 virtual:

class Shape {
   protected:
      int width, height;

   public:
      Shape( int a = 0, int b = 0){
         width = a;
         height = b;
      }
      virtual int area() {
         cout << "Parent class area :" << width * height << endl;
         return width * height;
      }
};

class Triangle: public Shape {
   public:
      Triangle( int a = 0, int b = 0):Shape(a, b) { }

      int area () {
         cout << "Triangle class area :" << (width * height)/2 << endl;
         return (width * height / 2);
      }
};

這是因為 C++ 在 class 建立時,預設會用 static linkage 綁定 function 的定義,而這個會導致當一個 function 被呼叫時,「只會根據其 variable type 來判斷該呼叫哪一個 function」。也就是說,無論實際上物件是 Shape 還是 Triangle ,只要他是 Shape 的變數,就會呼叫到 Shapearea()

如果將 function 設定為 virtual,C++ 就會知道該將 function 改用 dynamic linkage 處理,他會根據當下 object 的實際類別來判斷該呼叫哪一個 function (使用 vtable)。

簡單的比較例子:

class Base
{
  public:
            void Method1 ()  {  std::cout << "Base::Method1" << std::endl;  }
    virtual void Method2 ()  {  std::cout << "Base::Method2" << std::endl;  }
};

class Derived : public Base
{
  public:
    void Method1 ()          {  std::cout << "Derived::Method1" << std::endl;  }
    void Method2 () override {  std::cout << "Derived::Method2" << std::endl;  }
    // Note - override is optional; adding it to Method1 would result in an error
};

Base* basePtr = new Derived ();
// Note - constructed as Derived, but pointer stored as Base*

basePtr->Method1 ();  //  Prints "Base::Method1"
basePtr->Method2 ();  //  Prints "Derived::Method2"

參考資料: https://stackoverflow.com/questions/2391679/why-do-we-need-virtual-functions-in-c

Abstract Methods & Abstract Class

C++ 並沒有 abstract 關鍵字,但是可以用別的方式做到相同效果。

只要單純宣告但不定義 virtual function,並且加上 = 0 ,就稱之為 pure virtual function:

virtual int area() = 0;

這等同於 Java 的 abstract method。

只要 class 中包含至少一個 pure virtual function,那麼該 class 就不能被實體化,因此效果等同於 abstract class。

Class vs. Struct

C++ 的 class 與 struct 其實沒有太大的差別,只是有幾個關鍵差異:

  • Class 的 member 預設是 private,struct 是 public
    • 繼承的時候也類似,class 預設會是 private 繼承
  • Class 可以當作 template 的關鍵字用,struct 不行

C++ 進階語法

Try-Catch

C++ 可以 throw exception,而且可以用 try-catch 抓住。與 Java 最大的不同在於 C++ 的 throw 可以丟出任何種類的資料,catch 只要寫清楚資料的型別就好:

#include <iostream>
using namespace std;

double division(int a, int b) {
   if( b == 0 ) {
      throw "Division by zero condition!";
   }
   return (a/b);
}

int main () {
   int x = 50;
   int y = 0;
   double z = 0;
 
   try {
      z = division(x, y);
      cout << z << endl;
   } catch (const char* msg) {
     cerr << msg << endl;
   }

   return 0;
}

C++ STD 有內建一系列的 exception,可以在下面網頁看到清單:

https://cplusplus.com/reference/exception/exception/?kw=exception

另外官方也鼓勵開發者繼承 exception class 來製造自己的錯誤種類,其中只需要 override what() (顯示錯誤資訊的 function) 就好。

Template

Template 就是 Jave 的 generic,只是語法稍微不同,功能也沒那麼強而已。

定義 function template 的範例如下:

template <typename T>
T minimum(const T& lhs, const T& rhs)
{
    return lhs < rhs ? lhs : rhs;
}

定義 class template 的範例如下:

template <class T>
class Stack { 
   private: 
      vector<T> elems;    // elements 

   public: 
      void push(T const&);  // push element 
      void pop();               // pop element 
      T top() const;            // return top element 
      
      bool empty() const {      // return true if empty.
         return elems.empty(); 
      } 
}; 

template <class T>
void Stack<T>::push (T const& elem) { 
   // append copy of passed element 
   elems.push_back(elem);    
} 

注意當 class method 的定義沒有寫在 class 的宣告內時,也要額外宣告 template <class T> 的標籤。

MSFT 官網有針對 template 更加詳盡的介紹: https://learn.microsoft.com/zh-tw/cpp/cpp/templates-cpp?view=msvc-170

Modern C++ (C++11 之後的變化)

棄用語法

  • 字串 literal 應該改用 const char * 型別而不是 char * 型別宣告
  • register 的效果被移除(不再具有將變數放入 CPU register 的效果)
  • bool 不可以 ++
  • C 語言風格的轉換方式 (type) 被棄用,應該改用 static_castreinterpret_castconst_cast 等方式轉換

新增語法

C++11:

  • 指定 Null Pointer 應改用 nullptr 而不是 NULL

    • NULL 可能會被解析成 int 造成額外問題,例如有兩個 function 分別為 foo(int a)foo(char* b),當我們呼叫 foo(NULL) 就不見得會呼叫 foo(char* b) 那個。
  • 加入 constexpr:用來表示某一個 expression 或 function 輸出的結果是一個常數

    • 編譯器會去驗證這點,並且可以以此來優化程式碼

    • 範例:

      constexpr int fibonacci(const int n) {
          return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
      }
      
      constexpr int var_constexpr = 1 + 2 + 3;
      
  • 提供可以使用 initialization list 初始化任意物件的能力(但需要實作對應的 constructor)

    • 範例:
      #include <initializer_list>
      #include <vector>
      #include <iostream>
      
      class MagicFoo {
      public:
          std::vector<int> vec;
          MagicFoo(std::initializer_list<int> list) {
              for (std::initializer_list<int>::iterator it = list.begin();
                  it != list.end(); ++it)
                  vec.push_back(*it);
          }
      };
      int main() {
          // after C++11
          MagicFoo magicFoo = {1, 2, 3, 4, 5};
      
          std::cout << "magicFoo: ";
          for (std::vector<int>::iterator it = magicFoo.vec.begin(); 
              it != magicFoo.vec.end(); ++it) 
              std::cout << *it << std::endl;
      }
      
  • 加入 auto :自動型別推導

    auto i = 5;              // i is inferred as int
    auto arr = new auto(10); // arr is inferred as int *
    
    • auto 也常用於 template 指定 function return type 時,這樣就不需要額外定義另一個 type 變數:

      // Old way
      template<typename R, typename T, typename U>
      R add(T x, U y) {
          return x + y;
      }
      
      // After C++11
      template<typename T, typename U>
      auto add(T x, U y){
          return x + y;
      }
      
  • 加入 decltype : 輸出變數是何種型別

    • 通常搭配 std::is_same<T, U> 使用,檢查是否兩個型別是同一個型別

      auto x = 1;
      if (std::is_same<decltype(x), int>::value)
          std::cout << "type x == int" << std::endl;
      
  • Ranged for loop

    std::vector<int> vec = {1, 2, 3, 4};
    for (auto element : vec)
        std::cout << element << std::endl; // read only
    for (auto &element : vec) {
        element += 1;                      // writeable
    }
    
    • 宣告 ranged for loop 變數的原則:
      • 想 copy 數值 => 使用 auto x
      • 想直接引用原本的物件,並且可能會修改 => 使用 auto &x
      • 想直接引用原本的物件,但不會修改 => 使用 auto const &x
  • 搭配 template 使用別名:

    template<typename T, typename U>
    class MagicType {
    public:
        T dark;
        U magic;
    };
    
    template<typename T>
    using TrueDarkMagic = MagicType<std::vector<T>, std::string>;
    
    int main() {
        TrueDarkMagic<bool> you;
    }
    
  • Template 變動變數清單:

    template<typename... Ts>
    void magic(Ts... args) {
        std::cout << sizeof...(args) << std::endl;
    }
    
    magic(); // Print: 0
    magic(1); // Print: 1
    magic(1, ""); // Print: 2
    
  • Delegate constructor:允許重複使用同一個 class 內的 constructor

    class Base {
    public:
        int value1;
        int value2;
        Base() {
            value1 = 1;
        }
        Base(int value) : Base() { // delegate Base() constructor
            value2 = value;
        }
    };
    
  • 繼承 constructor:使用 using 關鍵字來使用 base class 的 constructor

    class Subclass : public Base {
    public:
        using Base::Base; // inheritance constructor
    };
    
  • 加入 override : 強制檢查是否複寫不存在的 virtual function

    struct Base {
        virtual void foo(int);
    };
    struct SubClass: Base {
        virtual void foo(int) override; // legal
        virtual void foo(float) override; // illegal, no such virtual function
    };
    
  • 加入 final : 阻止繼承或 override

    struct Base {
        virtual void foo() final;
    };
    struct SubClass1 final: Base {
    }; // legal
    
    struct SubClass2 : SubClass1 {
    }; // illegal, SubClass1 has final
    
    struct SubClass3: Base {
        void foo(); // illegal, foo has final
    };
    
  • 拒絕或明確要求預設實作(用於 constructor 或 operator 等等):

    class Magic {
        public:
        Magic() = default; // explicit let compiler use default constructor
        Magic& operator=(const Magic&) = delete; // explicit declare refuse constructor
        Magic(int magic_number);
    }
    
  • enum class

    • enum 不同處在於:
      • 不會預設可以轉換為 int
      • 多個 enum class 存在相同名稱的 variant 也不會產生名稱衝突
    enum class new_enum : unsigned int {
        value1,
        value2,
        value3 = 100,
        value4 = 100
    };
    

C++17:

  • 可以在 if 條件式宣告變數

    if (const auto itr = std::find(vec.begin(), vec.end(), 3);
        itr != vec.end()) {
        *itr = 4;
    }
    
  • 可以對 if statement 使用 constexpr

    #include <iostream>
    
    template<typename T>
    auto print_type_info(const T& t) {
        if constexpr (std::is_integral<T>::value) {
            return t + 1;
        } else {
            return t + 0.001;
        }
    }
    int main() {
        std::cout << print_type_info(5) << std::endl;
        std::cout << print_type_info(3.14) << std::endl;
    }
    
  • 宣告變數時可以解構資料,例如下面的 [x, y, z]

    #include <iostream>
    #include <tuple>
    
    std::tuple<int, double, std::string> f() {
        return std::make_tuple(1, 2.3, "456");
    }
    
    int main() {
        auto [x, y, z] = f();
        std::cout << x << ", " << y << ", " << z << std::endl;
        return 0;
    }
    
  • Template 變動長度參數展開式:

    #include <iostream>
    template<typename ... T>
    auto sum(T ... t) {
        return (t + ...);
    }
    int main() {
        std::cout << sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) << std::endl;
    }
    

C++20:

  • auto 可以用於 function signature:

    int add(auto x, auto y) {
        return x+y;
    }
    
    auto i = 5; // infered as int
    auto j = 6; // infered as int
    std::cout << add(i, j) << std::endl;
    

Lambda Function

Lambda function 指的是為了當下某個特定目的建立的匿名 function,作用通常不是為了減少重複程式碼,而是為了將某種程序做為參數傳遞出去。

語法:

[capture list] (parameter list) mutable(optional) exception attribute -> return type {
// function body
}

其中 [capture list] 代表從 lambda function 外部帶入的變數,注意該變數是在建立 lambda function 的當下就會複製,而不是在呼叫時被複製。

下面是一個 lambda function 的範例,注意建立 lambda function 的當下 value 的數值被 function 給捕獲並且複製一份。 因此就算後續 value 被改成 100 也不會造成影響。

void lambda_value_capture() {
    int value = 1;
    auto copy_value = [value] {
        return value;
    };
    value = 100;
    auto stored_value = copy_value();
    std::cout << "stored_value = " << stored_value << std::endl;
    // At this moment, stored_value == 1, and value == 100.
    // Because copy_value has copied when its was created.
}

Capture list 的格式如下:

  • [] 空清單
  • [name1, name2, ...] 最常見,捕獲一系列變數
  • [&] 捕獲外部參考(copy by reference),讓編譯器自行推斷要捕獲誰
  • [=] 捕獲外部值(copy by value),讓編譯器自行推斷要捕獲誰

C++14 加入可以在 capture list 中寫 expression 的能力:

#include <iostream>
#include <memory>  // std::make_unique
#include <utility> // std::move

void lambda_expression_capture() {
    auto important = std::make_unique<int>(1);
    auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int {
        return x+y+v1+(*v2);
    };
    std::cout << add(3,4) << std::endl;
}

C++14 開始可以在 lambda 使用 auto

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

add(1, 2);
add(1.1, 2.2);

C++11 加入 std::function,正式明確定義 function 為一種型別:

#include <functional>
#include <iostream>

int foo(int para) {
    return para;
}

int main() {
    // std::function wraps a function that take int paremeter and returns int value
    std::function<int(int)> func = foo;

    int important = 10;
    std::function<int(int)> func2 = [&](int value) -> int {
        return 1+value+important;
    };
    std::cout << func(10) << std::endl;
    std::cout << func2(10) << std::endl;
}

另外也加入了 std::bindstd::placeholder,用以延後傳遞參數的時機:

int foo(int a, int b, int c) {
    ;
}
int main() {
    // bind parameter 1, 2 on function foo,
    // and use std::placeholders::_1 as placeholder for the first parameter.
    auto bindFoo = std::bind(foo, std::placeholders::_1, 1,2);
    // when call bindFoo, we only need one param left
    bindFoo(1);
}

std::move

C++11 加入了右值參考 T && 的概念,用途是可以將一個即將被銷毀的數值轉移到另一個變數。

std::move 提供了將一個左值轉換為右值參考的能力,一旦在 expression 中的右側出現了右值參考,C++ 便不會呼叫 copy constructor,而是改呼叫 move constructor (編譯器預設會實作一個)。

Move constructor 的概念是盡可能地只複製 pointer,而不是整份資料,並同時將舊物件內的 pointer 清除(改成 nullptr ),以避免大量資料被複製的行為發生,這也是 std::move 最常見的用途。

考慮以下例子:

template <class T>
swap(T& a, T& b) {
    T tmp(a);   // we've made a second copy of a
    a = b;      // we've made a second copy of b (and discarded a copy of a)
    b = tmp;    // we've made a second copy of tmp (and discarded a copy of b)
}

在這個例子中,發生了三次資料複製。

我們可以使用 std::move 改寫來避免複製資料:

template <class T>
swap(T& a, T& b) {
    T tmp(std::move(a));
    a = std::move(b);   
    b = std::move(tmp);
}

如果 T 是一個資料結構,例如 vector<int> ,那裡面可能就包含大量資料,用第一版就會浪費大量的資源與時間。

如同 copy constructor,預設 compiler 會幫每一個型別實作一個 move constructor,作法基本上跟 copy constructor 差不多。只差在他不會複製指標指向的資料,而是單純將指標複製一份(做 shallow copy 而非 deep copy)。同時也會修改原本 class 的 member,把所有變數都清空(避免跟 moved 後的物件使用到相同的記憶體空間)。

新增的 Containers

  • std::array

    • 威力加強版的 array,提供了一些 container 的額外功能:
    std::array<int, 4> arr = {1, 2, 3, 4};
    
    arr.empty(); // check if container is empty
    arr.size();  // return the size of the container
    
    // iterator support
    for (auto &i : arr)
    {
        // ...
    }
    
    // use lambda expression for sort
    std::sort(arr.begin(), arr.end(), [](int a, int b) {
        return b < a;
    });
    
    // illegal, different than C-style array, std::array will not deduce to T*
    // int *arr_p = arr;
    
  • std::forward_list

    • 只有單向的 linked list
  • unordered_set / unordered_map 跟其他東西

    • Hash map 基底的實作,提供 O(1) 插入與查找
  • std::tuple

    • std::pair 威力加強版,允許任意數量的組合
    • 常用 function
      • std::get: Get the value of a position in the tuple
      • std::tie: tuple unpacking
    #include <iostream>
    #include <stdexcept>
    #include <string>
    #include <tuple>
     
    std::tuple<double, char, std::string> get_student(int id)
    {
        switch (id)
        {
            case 0: return {3.8, 'A', "Lisa Simpson"};
            case 1: return {2.9, 'C', "Milhouse Van Houten"};
            case 2: return {1.7, 'D', "Ralph Wiggum"};
            case 3: return {0.6, 'F', "Bart Simpson"};
        }
     
        throw std::invalid_argument("id");
    }
     
    int main()
    {
        const auto student0 = get_student(0);
        std::cout << "ID: 0, "
                  << "GPA: " << std::get<0>(student0) << ", "
                  << "grade: " << std::get<1>(student0) << ", "
                  << "name: " << std::get<2>(student0) << '\n';
     
        const auto student1 = get_student(1);
        std::cout << "ID: 1, "
                  << "GPA: " << std::get<double>(student1) << ", "
                  << "grade: " << std::get<char>(student1) << ", "
                  << "name: " << std::get<std::string>(student1) << '\n';
     
        double gpa2;
        char grade2;
        std::string name2;
        std::tie(gpa2, grade2, name2) = get_student(2);
        std::cout << "ID: 2, "
                  << "GPA: " << gpa2 << ", "
                  << "grade: " << grade2 << ", "
                  << "name: " << name2 << '\n';
     
        // C++17 structured binding:
        const auto [gpa3, grade3, name3] = get_student(3);
        std::cout << "ID: 3, "
                  << "GPA: " << gpa3 << ", "
                  << "grade: " << grade3 << ", "
                  << "name: " << name3 << '\n';
    }
    

Smart Pointers

Smart pointer 的加入大幅改進了 C++ 管理記憶體的彈性。以往 C++ 開發者如果沒有正確使用 pointer,則可能會造成該 pointer 指向非法空間 (segmentation fault) 或者 memory leak 等常見問題。如果 C++ 開發者使用 smart pointer,C++ 就能夠自動進行記憶體管理,並在適當的時機釋放該釋放的記憶體。

  • std::shared_ptr : 使用計數器管理記憶體的 pointer。

    • 原理:每次複製 pointer 時都會造成內部的計數器加一,pointer 被銷毀時計數器減一,當計數器歸零時(即所有的 pointer 都被銷毀時),就會自動釋放背後物件的記憶體空間。
      auto pointer = std::make_shared<int>(10);
      auto pointer2 = pointer; // reference count+1
      auto pointer3 = pointer; // reference count+1
      int *p = pointer.get();  // no increase of reference count
      
      std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl;   // 3
      std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 3
      std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 3
      
      pointer2.reset();
      std::cout << "reset pointer2:" << std::endl;
      
      std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl;   // 2
      std::cout << "pointer2.use_count() = " 
          << pointer2.use_count() << std::endl;                // pointer2 has reset, 0
      std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 2
      
  • std::unique_ptr : 不能被複製只能 move 的 pointer。

    • 原理:當每次進行 pointer assignment 的行為時,被限定只能用 move 而不能複製的 pointer。舊的 pointer 在 move 完後會當即失效,無法繼續使用。而當這個唯一的 pointer 被銷毀時,背後物件的記憶體也會跟著被釋放。

      #include <iostream> // std::cout
      #include <memory>
      
      int main()
      {
        auto pointer = std::make_unique<int>(10);  // make_unique, from C++14
        // auto pointer2 = pointer;                // copy is illegal
        auto pointer3 = std::move(pointer);        // move is OK
        std::cout << pointer << std::endl;
        std::cout << pointer3 << std::endl;
      }
      
  • std::weak_ptr : 弱指針,針對 shared_ptr 設計,建立時不會引起計數器加一。

    • shared_ptr 有個常見的問題是,如果有兩個 shared_ptr 互相指向對方,那麼就會造成計數器永遠不會變成 0:

      Cycle Share

      • 因此這時通常會讓其中一個 pointer 改成用 weak_ptr,這樣就 shared_ptr 就可以正常歸零。
    • 用法:直接 assign shared_ptrweak_ptr 即可

      std::shared_ptr<int> a = std::make_shared<int>(42);
      std::weak_ptr<int> b = a;
      

C++ Concurrency

std::thread

C++11 引入了標準的 thread 函式庫,用法如下:

#include <iostream>
#include <thread>

int main() {
    std::thread t([](){
        std::cout << "hello world." << std::endl;
    });
    t.join();
    return 0;
}

Locks

  • std::mutex : lock 的基本元件,提供 lock 與 unlock 功能建立 critical section
    • 不建議直接使用,因為需要記得手動 lock 與 unlock
  • std::lock_guard : std::mutex 的 wrapper,創建時自動 lock mutex,解構時自動 unlock
  • std::unique_lock : std::mutex 的 wrapper,創建時自動 lock,中途可以選擇 unlock 並再度 lock,解構時會自動 unlock
    • 建議優先選擇用這個

    • 範例:

      #include <iostream>
      #include <mutex>
      #include <thread>
      
      int v = 1;
      
      void critical_section(int change_v) {
          static std::mutex mtx;
          std::unique_lock<std::mutex> lock(mtx);
          // do contention operations
          v = change_v;
          std::cout << v << std::endl;
          // release the lock
          lock.unlock();
      
          // during this period, others are allowed to acquire v
      
          // start another group of contention operations
          // lock again
          lock.lock();
          v += 1;
          std::cout << v << std::endl;
          // Unlock when it disappears
      }
      
      int main() {
          std::thread t1(critical_section, 2), t2(critical_section, 3);
          t1.join();
          t2.join();
          return 0;
      }
      

std::future

一個用來建立 task,等待未來回收結果的 utility 元件。

詳情:https://changkun.de/modern-cpp/en-us/07-thread/#7-3-Future

std::condition_variable

提供 wait()notify_all()notify_one() 的功能,讓 thread 可以進入等待狀態,其他 thread 也可以叫醒等待的 thread

範例:https://changkun.de/modern-cpp/en-us/07-thread/#7-4-Condition-Variable

std::atomic

如果想要保證一些常見操作(例如 ++)的原子性,除了使用 std::mutex 建立 critical section 之外,也可以使用 std::atomic 。 這樣可以大幅減少要撰寫的程式碼。 下面展示使用 std::atomic 做出 atomic 的 ++ 操作的範例:

#include <atomic>
#include <thread>
#include <iostream>

std::atomic<int> count = {0};

int main() {
    std::thread t1([](){
        count.fetch_add(1);
    });
    std::thread t2([](){
        count++;        // identical to fetch_add
        count += 1;     // identical to fetch_add
    });
    t1.join();
    t2.join();
    std::cout << count << std::endl;
    return 0;
}

因為 std::atomic 實際上是一個 template,可以容納任何類型的資料,因此 C++ 有提供額外的 API 檢查該類型資料是否有保障原子性:

#include <atomic>
#include <iostream>

struct A {
    float x;
    int y;
    long long z;
};

int main() {
    std::atomic<A> a;
    std::cout << std::boolalpha << a.is_lock_free() << std::endl;
    return 0;
}