C++ 標準自從建立後到現在已經經過了許多次改版,其中最重要的版本除了 C++98 外,當在2011 年正式發佈的 C++11 標準了。該標準加入了許多新特性,不但讓程式撰寫更為精簡,在執行時效率也更加上升,可說是之後 C++14 與 C++17 等標準的基石。本篇文章簡單整理了一些關於 C++11 重要標準變更與概念,若要詳細了解相關內容以及 C++14/17 的相關變更可參考 Wikipedia 等網站。

語法變更

Auto and decltype

C++11 可使用 Auto 來自動推導宣告的變數型別,藉以減少需要撰寫的資料長度。

// C++03
float x = 3.0f;
vector<string> vec = vector<string>();

// C++11, 自動型別推導
auto x_11 = 3.0f;   // x_11 is float type
auto vec_11 = vector<std::string>();

而關鍵字 decltype 可由已存在變數的推導出型別

float f = 30.0f;
decltype(f) fc; // variable 'fc' is float type

實作上 decltype 常與 auto 並用,如自動推導 template function 的返回型別等。

template<class Fun, class... Args>
decltype(auto) Example(Fun fun, Args&&... args)
{
    return fun(std::forward<Args>(args)...);
}

Constexpr

C++11 可透過 constexpr 來限制 function 行為,讓編譯器可以在編譯時提早決定數值。

template<int T> struct K {};

// C++03
int getValue(int x) { return x + 5;}
auto ss = K<getValue(3)>(); // Compiler error !

// C++11
constexpr int getValue(int x) { return x + 5;}
auto ss = K<getValue(3)>(); // pass !!

Null pointer constant

過去 C++ 標準並沒有定義空指標的代表關鍵字,而常用的 NULL 表示式也常常只是使用 macro 將 NULL 定義為 0 而已,因此 C++11 在標準中加入了 nullptr 關鍵字來定義空指標。

// C++03
int* p03 = NULL;

// C++11
int* p11 = nullptr

Range-based for loop

auto vec = vector<int>{1, 2, 3, 4, 5};
// C++03
for (int i = 0; i < 5; ++i) {
    cout << array[i] << endl;
}
// C++11
for (auto& r: vec) {
    cout << r << endl;
}

Initializer lists and uniform initialization

在 C++03 中,如要對 vector 等物件進行初始化時需要另外執行 push_back() 動作。假如想建立一個內部包含 1 ~ 3 的整數數字的 vecotor<int>,需要如下範例建立 vec 物件

// C++03
vector<int> vec = vector<int>();
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);

而在 C++11 中可利用 std::initializer_list ,並透過 {} 達成 uniform initialization。

// C++11, simple !
auto vec = vector<int>{1, 2, 3};

若資料為嵌套的類型,也可用相同方式初始化

auto vec_in = vector<vector<int>>{ {1}, {2, 3}, {4, 5, 6} };
auto pairs = vector<tuple<int, float>>{ {1, 1.1f}, {2, 2.2f}, {3, 3.3f} };

R-Value Reference (右值參照) 與 Move Semantic

在 C++ 的定義中,左值為可以透過記憶體參照對應到的物件,其他則為右值。也可以把右值想像成一個臨時的數值,左值可以被賦值並修改但右值不行。因 C++ 為值語意程式語言,在物件的建立上常會有多餘的建構與複製動作而倒置效能上的損失。而 C++11 中加入了右值參照功能,透過關鍵字 && 來區別右值。

// C++03, L-Value reference
void ref(int& x) {
    cout << "Left value: " << x << endl;
}

x = 3;
ref(x); // print: "Left value: 3"
ref(3); // complier error !

// C++11, R-Value reference
void ref(int&& x) {
    cout << "Right value" << endl;
}
x = 3;
ref(x); // print: "Left value: 3"
ref(3); // print: "Right value: 3"

因可區別左值與右值,我們能使用右值參照達成 move semantic 的效果。右值可以想像成是個暫時的值,在達成任務後就會消失,那我們可以去將右值內部的資料直接移動到新物件中,避免過多的複製動作已增加程式效能。先定義一個簡單的MoveVec 結構

struct MoveVec {
    std::vector<int> vec;

    MoveVec() = default;
    MoveVec(const vector<int>& init): vec(init) {} // Copy constructor
    MoveVec(vector<int>&& init): vec(std::forward<vector<int>>(init)) {} // Move constructor
};

MoveVec 可使用一個 vector<int> 來初始化內部的 vec 物件。在 C++03 時需先在外部建立一個暫時的 vector<int> 物件,並使用它來初始化 MoveVec 時,會多出一次複製動作來將外部的 vector<int> 物件中的值複製到MoveVec中的vec 內部,無形中多增加了一次複製成本。在 C++11 可使用 move constoructor,將暫時物件的值移動到內部的vector<int> 資料中,避免多一次複製動作。

// C++03
auto vec_out = vector<int>{1, 2, 3}; // Initial setting
auto obj = MoveVec(vec_out); // Call copy constructor

// C++11
auto obj_move = MoveVec(vector<int>{1, 2, 3}); // Call move constructor

其中 vec_out 物件因為是左值,因此所呼叫的函式為 copy constructor 將物件內容複製到 vec 中。而建構 obj_move 物件時因傳入的參數為右值,因此會呼叫 move constructor 直接將傳入的 vector<int> 內部資源移動到vec 中,而不用在將所有資料複製一次。此外也可以使用的標準庫函式中的 std::move 函式,將左值物件強制轉換為右值參照以呼叫對應的函式。

auto obj_out = MoveVec(std::move(vec_out)); 
cout << vec_out.size(); // print: "0",因 vec_out 的資料比被"移動"到 obj_out 中的 vec 內。

右值參照是 C++11 中加入的一個重要特徵,更詳細的內容可參考 Wikipedia 的詳細介紹。

Lambda functions and expressions

支援 Lambda expression 也是 C++11 中的一項重大修改。C++ 標準庫中有許多需要傳入計算式的運用,過去 C++ 需要另外創建函數才能執行,會造成撰寫 code 上的麻煩。而 C++11 支援 lambda expression,可直接創建匿名函式來執行需要的功能。現建立一個資料集

auto vec = vector<int>{1, 2, 3, 4, 5};

若要使用 for_each 對每個數值加ㄧ後輸出,C++03 中需要以下的方式執行

// C++03
void plus(int x) { cout << x + 1 << endl; }
for_each(begin(vec), end(vec), plus());

由於需要另外撰寫函數或 Functor ,無形中增加了實作上的麻煩,而 C++11 中的 lambda expression 可使用簡單撰寫匿名函式來傳入 for_each 中達成同樣的效果

// C++11, Lambda expression
for_each(begin(vec), end(vec), [](auto& x){ cout << x + 1 << ","; });
// print: 1,2,3

此外也可使用 & 參數來引入外部參數

int factor = 42;
std::for_each(begin(vec), end(vec), [&factor](int x) { cout << x + factor << ",";});
// print: 43,44,45

Variadic Template

Variadic template 也可翻可變參數模板,也是 C++11 中的一項重要變更。過去宣告 template 時參數數量需為固定,因此對於可變參數數量的結構就需要定義許多 template 結構。而在 C++11 中導入的 variadic template 可建立可變數量的參數結構,使用時可依照定義好的數量展開該結構。C++11 之後的可變參數 template 幾乎都使用了 variadic template 語法來接受可變數量參數。

Variadic template 基本宣告方式如下

template<typename... Ts>

其中 <typename... Ts> 代表 Parameter Pack ,也就是一連串的參數列。以此範例來說

template<typename T>
T adder(T v) {
  return v;
}

template<typename T, typename... Ts>
T adder(T first, Ts... args) {
    return first + adder(args...);
}

函式 adder 可接受不同長度的參數加總後回傳結果,下圖可為執行結果

cout << adder(1, 2, 3); // output: 6  
cout << adder(4.2, 1, 2, 5, 7.3); // output: 19.5
cout << adder(1, 2, 1.3); // output: 4

adder(1, 2, 3) 被呼叫時,編譯器會將 function 展開為如下的形式

// adder(1, 2, 3) 呼叫的 funciton
int adder(int first, int second, int third) {
    return first + adder(second, third);
}

// 上面函式中的 adder(second, third) 會再展開為下面型式
int adder(int first, int second) {
    return first + adder(second);
}

// 上面函示中的 add(second) 則會依照終止樣板展開為以下形式
int adder(int first) {
    return first;
}

因此當 adder(1, 2, 3) 被呼叫後,會將內容參數加總後回傳。且因為回傳型態跟呼叫的順序與參數內容有關,展開的 function 也會不同。如 adder(4.2, 1, 2, 5, 7.3) 因第一個數字是 double 型態,因此加總後會可正確回傳 double 型態數字 19.5,但如果呼叫 adder(1, 2, 1.3) 時因為最後一個回傳的型別是 int,因此小數點會被無條件捨去,因此只回傳 4 而不是 4.3

除了 function 外,class 也可用相同的擴展方式建立 variadic template,如 std::tuple 就是一個例子,可建立一個內含不同參數的 tuple 結構,並透過 std::get<> 來取出內容,如下所示:

// 建立一 tuple 結構
auto tp = tuple<string, int, vector<int>>("Hello", 5, {42, 2});
cout << get<0>(tp); // output: "Hello"
cout << get<1>(tp); // output: 5

Explicit overrides and final

C++11 可透過關鍵字 final 顯示宣告來禁止 class 或 function 被 override

// Class "final"
class Base final {};
class Derive: Base {}; // compiler error!

// Funcion "final"
class Base {
    virtual void show() final;
};

class Derive: Base {
    virtual void show(); // compiler error
}

此外也可使用 override 關鍵字來明確定義該函式為 override function

class Base {
    virtual void show();
}

class Derive: Base {
    virtual void show2() override; // compiler error
    virtual void show() override; // success
}

Explicitly defaulted and deleted special member functions

C++ 在類別建立時,自動建立如 default constructor, copy constructor 等預設函數,但有時候我們不希望該函數被建立,在過去需要宣告該函式為 private。而在 C++11 中支援了兩個關鍵字 defaultdelete 來控制預設建構式是否要建立。

class Base {
    Base() = default; // default constructor 使用預設的行為
    Base(const Base& base) = delete; // 不生成 copy constructor
};

此外由於 C++ 類別中的預設函數生成條件很複雜,可參考 rule of five, rule of zero 等方法決定是否要宣告該預設函式。

New typedef 語法

// C++03
typedef int DefInt;
// C++11
using DefInt = int;

新標準庫

Smart Pointer

C++11 也新增了許多標準函式庫內容。其中一個很重要的是智慧指標(smart point)的支援。過去的 C++ 指標在建立後需手動使用 delete 來刪除該 pointer 否則會出現 memory leak。

// C++03
int* p = new int(3);
// ... do something
delete p;  // release memory

而 Smart Pointer 則可幫助使用者管理資源且表面行為與指標的物件。以 shared_ptr 來說,真正的 pointer 包裝在該物件中,且可使用與一般 pointer 相同的呼叫方式使用 shared_ptr

// C++11
auto ptr = shared_ptr<int>(new int(2)); // Use shared point
cout << *ptr << endl; // output: 2

auto ptr_make = make_shared<int>(42);  // Use make_shared() function
cout << *ptr_make << endl;  // output: 42

此外過去在管理指標時有可能資源已經被 delete,但某些變數仍保有指摽位置,這會導致程式崩潰。而 shared_ptr 內部具有 reference counting 的功能,也就是能記錄有多少個物件被參照到資源,等到都沒有參照到資源時才會執行資源的 delete 動作。

void ref(shared_ptr<int> ptr) {
    // reference counts = 3 (+1)
    // do something...
    // when leave block, reference counts = 2 (-1)
}

auto pt = make_shared<int>(3);  // reference count = 1 (+1)
auto pt2 = pt;  // reference counts = 2 (+1)
ref(pt);

// When leaving block
// Call pt2 destructor, reference count = 1 (-1)
// Call pt destructor, reference count = 0 (-1), delete resource.

C++11 新增的 Smart point 主要有 shared_ptrunique_ptrweak_ptr。本節只介紹 shared_ptr 的行為,unique_ptrweak_ptr 可自行參考相關資料

Concurrency

由於 C++03 不支援多執行緒功能,因此若要使用多執行緒執行則需要透過 OpenMP 或系統 API 等外部呼叫多執行緒函式。而從 C++11 之後也正式在標準庫中加入多執行緒支援,讓使用者可更簡單的使用。下圖為簡單的使用方式

#include <thread>

void thread_1(int x) {
    cout << "call thread_1() : " << x << endl;
}
void thread_2() {
    cout << "Call thread_2()" << endl;
}

int main(int argc, char *argv[]) {
    thread th1(&thread_1, 3);   // 建立 thread 物件 th1 並開始執行
    thread th2(&thread_2);  // 建立 thread 物件 th1

    cout << "main thread" << endl;

    th1.join(); // 一定要加入,宣告 th1() 必須執行完畢後才能往下執行。
    th2.join(); // 同上
}

當上面的資料執行時,thread_1()thread_2() 可能會有不同的執行順序,或當一邊執行時會切換到另一邊,如下面的執行範例。

// Output example - 1
call thread_1() : Call thread_2()main thread3

// Output example - 2
main thread
call thread_1() : Call thread_2()
3

可看出因是在不同執行緒中運作,因此執行順序有可能被打亂。而除了上述的基本使用法,其他進階用法可自行參考其他資訊

Function Objects

C++11 新增的 std::function 類別,可以將 function 當作物件般儲存操作與傳遞。而 std::bind 則是能將不同的 function 綁定依照特定順序綁定,重新建立一個函式物件。上面兩類可透過 #include <functional> 引入程式碼中使用。

std::function

#include <functional>

int sumXY(int x, int y) {
    return x + y;
}

// 使用現有函數建立 
std::function<int(int,int)> sum = sumXY;
cout << sum(1, 3) << endl; // output: 4

// 使用 lambda expression 建立
std::function<void(int)> disp = [](int x) {cout << x << endl;};
disp(42);  // output: 42

// 使用 auto 自動建立 function object
auto sum_2 = [](int x, int y) -> float {return x + y + 1.1; };
cout << sum_2(2, 4) << endl; // output: 7.1

std::bind

#include <fnuctional>

int mulXY(int x, int y) {
    return x * y;
}

// sum_b(z) = sumXY(8, z)
auto sum_b = std::bind(sumXY, 8, std::placeholders::_1);
cout << sum_b(10) << endl;  // output: 18

using namespace std::placeholders;

// mul_b(x, y) = mulXY(sumXY(x, y), y)
auto mul_b = std::bind(mulXY, std::bind(sumXY, _1, _2), _2);
cout << mul_b(2, 3) << endl;    // output: 15

Reference