C++11 語言特性精要 (C++11 Language Features)
Overview Table
| 特性 | 核心要點 | 執行緒庫中的應用 |
|---|---|---|
右值引用 && / 移動語意 |
只綁右值;移動建構子偷資源而非複製 | std::thread、std::unique_lock<> 等只移動型別的所有權轉移 |
| 完美轉發 | 模板 T&& + std::forward<T> 保留引數值類別 |
std::thread 建構子轉發可呼叫物件與引數 |
刪除函式 =delete |
禁用特定函式/重載,編譯期報錯 | std::mutex、std::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; }
};
- 對左值做顯式移動:
std::move(x)或static_cast<X&&>move只是轉型,本身不移動任何東西。 - 只移動型別:
std::thread、std::unique_lock<>、std::future<>、std::promise<>、std::packaged_task<>、std::unique_ptr<>皆不可複製、可移動;std::string、std::vector<>兩者皆可(移動用來避免大量資料複製)。
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;
};
- 刪除複製建構子/複製賦值後,再自行提供移動版本 → 只移動類別(如
std::thread、std::unique_lock<>)。 - 刪除函式仍參與重載決議,可用來封殺特定型別轉換:
void foo(short);
void foo(int) = delete;
foo(42); // 錯誤,選中已刪除的 int 版本
foo((short)42); // OK
預設函式 =default 與 trivial (A.3)
= default 要求編譯器生成該特殊成員函式(預設/複製/移動建構子、複製/移動賦值、解構子)。使用動機:
- 改變存取層級(生成版本預設 public,可宣告在 private/protected)
- 當作文件,明示「預設行為即正確」
- 找回被抑制的函式(寫了其他建構子後,預設建構子不再自動生成;
X()=default;可強制生成並保留其特殊性質) - 生成虛解構子、宣告非 const 引用參數的複製建構子等
trivial(簡單)的意義:只有「編譯器生成」的版本才可能是 trivial;使用者自行實作的版本永遠不是。trivial 帶來的能力:
| trivial 條件 | 帶來的能力 |
|---|---|
| trivial 複製建構/賦值/解構 | 可用 memcpy/memmove 直接搬移 |
| trivial 建構、複製、解構 | 可作為字面值型別用於 constexpr 函式 |
| trivial 特殊成員 | 可放入含使用者定義建構/解構的 union |
| trivial 複製賦值 | 可用於 std::atomic<T> 的型別 T |
- 無使用者提供建構子的類別是 aggregate,可用聚合初始化
aggregate x={42,3.141};。 - 預設初始化的怪癖:
struct X{int a;};時X x1;的a值不確定(靜態儲存期則為 0),但X x2=X();會把a零初始化。自行撰寫建構子會喪失此規則;X::X()=default;則保留。原子型別即依賴此特性 + constexpr 建構子達成靜態初始化。
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)。
- constexpr 物件:
constexpr int i=45;宣告的物件為 const,且必須以常數運算式初始化。 - constexpr + 模板:函式模板宣告為 constexpr 時,若某次實例化的參數/回傳型別非字面值型別,編譯器就忽略 constexpr(該實例化退化為普通函式,不算錯誤)。
Lambda 函式 (A.5)
就地定義的可呼叫物件,適合作條件變數謂詞、std::thread 執行緒函式等:
std::unique_lock<std::mutex> lk(m);
cond.wait(lk, []{ return data_ready; }); // 回傳型別自動推導為 bool
- 回傳型別推導:函式體只有一個 return 時自動推導;多個 return 必須顯式指定
[]()->bool{...}(此時就算無參數也要寫空括號)。 - 捕獲列表 (capture list) 各形式:
| 形式 | 意義 |
|---|---|
[] |
不捕獲任何區域變數(只能用全域) |
[=] |
全部以複製捕獲(建立時拍快照) |
[&] |
全部以引用捕獲 |
[=, &j, &k] |
預設複製,j、k 例外用引用 |
[&, j, k] |
預設引用,j、k 例外用複製 |
[i, &j] |
只捕獲列出的變數(未列出者不可用,拼錯編譯報錯) |
[this] |
捕獲 this 指標,才能存取類別成員變數(成員不能直接捕獲) |
[p=std::move(p)] |
C++14 初始化捕獲:移動只移動型別進 lambda |
- C++14 泛型 lambda:參數寫
auto,呼叫時才推導,auto f=[](auto x){...};。
[&] 捕獲的區域變數若在 lambda 被呼叫前銷毀(例如 lambda 交給 detach 的執行緒),為未定義行為。lambda 壽命超過區域範圍時,應改用複製或初始化捕獲(移動),參見 02-Thread-Management/02-Passing-Arguments-and-Transferring-Ownership。
變參模板 (A.6)
模板可接受不定數量的參數,以省略號宣告參數包 (parameter pack):
template<typename... Params>
class my_template {};
- 部分特化中的模式匹配:
std::packaged_task<>主模板只有一個參數,實際定義在部分特化中:
template<typename ReturnType, typename... Args> class packaged_task<ReturnType(Args...)>;
於是std::packaged_task<intstring,double>中 Args 捕獲到std::string, double。 - 包展開:
Params...出現處,包中每一項會代入其所在的型別模式,逐項產生結果:
| 展開運算式(設 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>> |
- 函式參數包 + 完美轉發是最常見組合thread 建構子原型:
template<typename CallableType, typename... Args>
thread::thread(CallableType&& func, Args&&... args);
sizeof...(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 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; |
控制流首次通過宣告時(同區域靜態變數,但每執行緒各一份);未呼叫該函式的執行緒不會建構 |
生命週期規則:
- 解構子在建構它的那個執行緒結束時呼叫,順序為建構的逆序(初始化順序未指定時,勿讓解構互相依賴)。
- 建構或解構中擲出例外 →
std::terminate()終止程式。 - 呼叫
std::exit()或從main()返回時,該執行緒的 thread_local 會銷毀;程式退出時仍在執行的其他執行緒,其 thread_local 解構子不會被呼叫。 - 可取得指向 thread_local 的一般指標傳給其他執行緒使用,但擁有者執行緒結束後解引用為未定義行為。
類別模板參數推導 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>
- 對可能推導出錯誤型別的模板,作者可撰寫推導指引 (deduction guide) 修正(超出本書範圍)。
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 由建構子引數推導 |
Related Notes
- 12-Appendix/02-Concurrency-Library-Comparison
- 12-Appendix/03-ATM-Message-Passing-Example
- 12-Appendix/Practice-Appendix
- 02-Thread-Management/02-Passing-Arguments-and-Transferring-Ownership — 移動語意於
std::thread所有權轉移的實戰 - 04-Synchronizing-Operations/01-Condition-Variables-and-Thread-Safe-Queue — lambda 作為條件變數謂詞
- 05-Memory-Model-and-Atomics/02-Atomic-Types-and-Operations — trivial 複製賦值與
std::atomic<>、constexpr 靜態初始化