C++11 語言特性精要 (C++11 Language Features)

Overview Table

特性 核心要點 執行緒庫中的應用
右值引用 && / 移動語意 只綁右值;移動建構子偷資源而非複製 std::threadstd::unique_lock<> 等只移動型別的所有權轉移
完美轉發 模板 T&& + std::forward<T> 保留引數值類別 std::thread 建構子轉發可呼叫物件與引數
刪除函式 =delete 禁用特定函式/重載,編譯期報錯 std::mutexstd::thread 禁止複製
預設函式 =default 要求編譯器生成;唯有生成版本可為 trivial std::atomic<> 要求 trivial 複製賦值
constexpr 函式 可用於常數運算式;constexpr 建構子 → 靜態初始化 std::mutex 預設建構子為 constexpr,避免初始化競爭
Lambda 就地定義可呼叫物件;捕獲列表控制存取 條件變數謂詞、執行緒函式、小任務打包
變參模板 參數包 ... 展開、sizeof... std::thread/std::packaged_task<> 建構子
auto 依函式模板引數推導規則推導型別 簡化冗長 iterator 型別宣告
thread_local 每執行緒一份獨立實例 執行緒本地快取、亂數種子等
CTAD(C++17) 由建構子引數推導類別模板參數 std::lock_guard guard(m);std::scoped_lock

右值引用與移動語意 (A.1)

左值 (lvalue) 是有名字、有明確位址的物件;右值 (rvalue) 是字面常量、臨時物件等。左值引用 T& 只能綁左值(const T& 可綁右值是特例);右值引用 T&& 只能綁右值,無法綁左值。

int&& i = 42;   // OK,右值
int j = 42;
int&& k = j;    // 編譯失敗,j 是左值

移動語意 (move semantics):右值通常是臨時的、即將銷毀,與其複製不如直接「偷走」其內部資源。移動建構子只搬指標並把來源清空:

class X {
    int* data;
public:
    X(const X& other): data(new int[1000000])          // 複製:配置+拷貝
    { std::copy(other.data, other.data+1000000, data); }
    X(X&& other): data(other.data)                      // 移動:偷指標
    { other.data = nullptr; }
};
具名右值引用參數在函式內是「左值」

void do_stuff(X&& x_) 中,X a(x_); 呼叫的是複製建構子;要繼續移動必須寫 X bmove(x_);

被移動後的狀態

標準庫物件被移動後保持「有效但未指定」的狀態:std::thread 移動後等同預設建構;std::string 移動後內容不保證為何,使用前不可假設其值。

函式模板中的右值引用與完美轉發 (A.1.2)

當模板參數宣告為 T&&,推導規則特殊:

呼叫 T 推導為 參數實際型別
foo(42);(右值) int int&&
int i; foo(i);(左值) int&(左值引用) int&(引用摺疊)

因此同一個模板既能收左值也能收右值。要把引數「原封不動」轉交給下一層,用 std::forward<T>(完美轉發):

template<typename... ArgTypes>
void bar(ArgTypes&&... args)
{
    fooforward<ArgTypes>(args)...;  // 左值仍為左值、右值仍為右值
}

std::thread 建構子即用此模式,右值引數直接移入內部儲存,免去複製。

刪除函式 =delete (A.2)

在函式宣告後加 = delete,該函式即不可使用;呼叫時在編譯期報錯(舊法「private + 不實作」只能拖到連結期)。

class no_copies {
public:
    no_copies() {}
    no_copies(no_copies const&) = delete;
    no_copies& operator=(no_copies const&) = delete;
};
void foo(short);
void foo(int) = delete;
foo(42);        // 錯誤,選中已刪除的 int 版本
foo((short)42); // OK

預設函式 =default 與 trivial (A.3)

= default 要求編譯器生成該特殊成員函式(預設/複製/移動建構子、複製/移動賦值、解構子)。使用動機:

trivial(簡單)的意義:只有「編譯器生成」的版本才可能是 trivial;使用者自行實作的版本永遠不是。trivial 帶來的能力:

trivial 條件 帶來的能力
trivial 複製建構/賦值/解構 可用 memcpy/memmove 直接搬移
trivial 建構、複製、解構 可作為字面值型別用於 constexpr 函式
trivial 特殊成員 可放入含使用者定義建構/解構的 union
trivial 複製賦值 可用於 std::atomic<T> 的型別 T

constexpr 函式 (A.4)

常數運算式才能做的事:指定陣列長度非型別模板參數、類別內 static const 整數成員初始化、聚合的靜態初始化constexpr 讓符合條件的函式可在編譯期求值:

constexpr int square(int x) { return x*x; }
int array[square(5)];   // OK,25 個元素
int dummy = 4;
int array2[square(dummy)]; // 錯誤:dummy 非常數運算式,此時 square 只是普通函式呼叫

字面值型別 (literal type) 才能用於常數運算式,條件:trivial 複製建構子、trivial 解構子、所有非靜態成員與基底類別皆為 trivial 型別、具 trivial 預設建構子或 constexpr 建構子

C++11 對 constexpr 函式的要求(C++14 大幅放寬):

C++11 C++14 起
函式體只能有一個 return 允許多個 return、迴圈、if/switch
純函式、無副作用 仍須無副作用(不得修改非區域物件),但可修改函式內建立的物件
constexpr 成員函式隱含 const 不再隱含 const

constexpr 建構子(C++11):函式體必須為空、初始化列表初始化所有基底與非靜態成員、列表中所有運算式皆為常數運算式。

與併發最相關的意義:靜態初始化消除初始化競爭

constexpr 建構子 + 常數運算式引數 → 物件屬於常數/靜態初始化,保證在任何程式碼執行前完成。std::mutex 的預設建構子因此被要求為 constexpr——全域互斥鎖若是動態初始化,可能在「鎖還沒初始化完」時就被其他執行緒拿去用,產生初始化上的競爭條件 (race condition)。std::atomic<> 亦同(見 05-Memory-Model-and-Atomics/02-Atomic-Types-and-Operations)。

Lambda 函式 (A.5)

就地定義的可呼叫物件,適合作條件變數謂詞、std::thread 執行緒函式等:

std::unique_lock<std::mutex> lk(m);
cond.wait(lk, []{ return data_ready; });   // 回傳型別自動推導為 bool
形式 意義
[] 不捕獲任何區域變數(只能用全域)
[=] 全部以複製捕獲(建立時拍快照)
[&] 全部以引用捕獲
[=, &j, &k] 預設複製,j、k 例外用引用
[&, j, k] 預設引用,j、k 例外用複製
[i, &j] 只捕獲列出的變數(未列出者不可用,拼錯編譯報錯)
[this] 捕獲 this 指標,才能存取類別成員變數(成員不能直接捕獲)
[p=std::move(p)] C++14 初始化捕獲:移動只移動型別進 lambda
引用捕獲的懸空風險

[&] 捕獲的區域變數若在 lambda 被呼叫前銷毀(例如 lambda 交給 detach 的執行緒),為未定義行為。lambda 壽命超過區域範圍時,應改用複製或初始化捕獲(移動),參見 02-Thread-Management/02-Passing-Arguments-and-Transferring-Ownership

變參模板 (A.6)

模板可接受不定數量的參數,以省略號宣告參數包 (parameter pack):

template<typename... Params>
class my_template {};
展開運算式(設 Params = int, char) 結果
std::tuple<Params...> std::tuple<int, char>
std::tuple<std::string, Params...> std::tuple<std::string, int, char>
std::tuple<Params* ...> std::tuple<int*, char*>
std::tuple<std::unique_ptr<Params>...> std::tuple<std::unique_ptr<int>, std::unique_ptr<char>>
template<typename CallableType, typename... Args>
thread::thread(CallableType&& func, Args&&... args);

auto 推導規則 (A.7)

auto 依「函式模板參數」的推導規則,由初始化式推導變數型別:

auto i = 42;        // int
auto& j = i;        // int&
auto const k = i;   // int const
auto* const p = &i; // int* const
int arr[45];
auto p2 = arr;      // int*  —— 陣列退化為指標
int& r = *p2;
auto x = r;         // int   —— 引用被去除
auto& y = r;        // int&  —— 要引用需明寫 auto&
auto 會丟棄引用與頂層 const

auto x = r; 得到的是複製;想避免複製或保留可修改性,必須寫 auto& / auto const&

thread_local 執行緒本地變數 (A.8)

thread_local每個執行緒擁有變數的獨立實例。可宣告的三類變數與初始化時機:

種類 範例 建構時機
命名空間作用域變數 thread_local int x; 該執行緒首次使用前(可能於執行緒啟動時,也可能延後;不保證確切時間點)
類別靜態成員 static thread_local std::string X::s;(類別內宣告 + 類別外定義) 同上
函式區域變數 thread_local std::vector<int> v; 控制流首次通過宣告時(同區域靜態變數,但每執行緒各一份);未呼叫該函式的執行緒不會建構

生命週期規則:

類別模板參數推導 CTAD (A.9)

C++17 起,以類別模板宣告物件而不寫模板參數列表時,依函式模板的推導規則、從建構子引數推導:

std::mutex m;
std::lock_guard guard(m);        // 推導為 std::lock_guard<std::mutex>

std::mutex m1; std::shared_mutex m2;
std::scoped_lock guard2(m1, m2); // 推導為 std::scoped_lock<std::mutex, std::shared_mutex>

Exam/Test Patterns

情境/關鍵字 答案
「只移動型別如何轉移所有權」 std::move()(等價 static_cast<T&&>);複製建構子 =delete
「函式內具名右值引用參數再傳遞」 它是左值,需再套 std::move/std::forward
「模板 T&& 傳入左值,T 是什麼」 左值引用(引用摺疊);用 std::forward<T> 完美轉發
「禁止特定重載/隱式轉換」 對該重載 =delete(仍參與重載決議)
「memcpy 可複製 / 可放進 std::atomic<>」 trivial(僅編譯器生成、=default 的函式可為 trivial)
「全域 std::mutex 為何無初始化競爭」 constexpr 建構子 → 常數/靜態初始化,程式碼執行前完成
「lambda 有多個 return」 必須顯式回傳型別 []()->T{...}
「lambda 存取類別成員」 捕獲 [this];成員變數不能直接捕獲
「取得參數包元素個數」 sizeof...(Args),為常數運算式
「每執行緒一份的變數」 thread_local;執行緒結束時逆序解構
std::lock_guard guard(m); 沒寫 <std::mutex> 為何合法」 C++17 CTAD 由建構子引數推導