附錄練習題 (Practice - Appendix)
Related Concepts
- 12-Appendix/01-Cpp11-Language-Features
- 12-Appendix/02-Concurrency-Library-Comparison
- 12-Appendix/03-ATM-Message-Passing-Example
| 關鍵字 | 答案 |
|---|---|
| 只移動型別 | 複製 =delete + 提供移動建構/賦值;轉移用 std::move |
模板 T&& 收到左值 |
T 推導為左值引用;用 std::forward<T> 完美轉發 |
| trivial 函式 | 僅編譯器生成(=default)版本可 trivial;可 memcpy、用於字面值型別與 std::atomic<> |
| 靜態初始化避免競爭 | constexpr 建構子mutex 即如此 |
| lambda 多個 return | 顯式回傳型別 []()->T{...} |
sizeof...(Args) |
參數包元素個數,為常數運算式 |
| thread_local 解構 | 執行緒結束時,建構的逆序 |
close_queue |
dispatcher 擲例外結束訊息迴圈 |
| 訊息無 handler 匹配 | 沿 prev 鏈回溯,最終被丟棄 |
| Boost/POSIX 原子操作 | 皆 N/A |
Question 1 - 右值引用與移動 [recall]
void do_stuff(X&& x_)可以用do_stuff(X());呼叫,卻不能用左值do_stuff(x);呼叫;而且在函式體內X a(x_);呼叫的竟是複製建構子。請解釋這兩個現象,並說明如何把x_真正移動出去。
右值引用 X&& 只能綁定右值,X() 是臨時物件(右值)所以可以,具名變數 x 是左值所以編譯失敗。而具名的右值引用參數在函式內部是左值(它有名字、有位址),因此 X a(x_); 走複製建構子;要移動必須顯式寫 X bmove(x_));(或 static_cast<X&&>(x_)。std::move 本身只是轉型,不做任何移動。
Question 2 - 刪除函式的兩種用途 [recall]
(1) 想讓類別不可複製、只可移動;(2) 想讓
void foo(short);拒絕foo(42);這種 int 引數(避免隱式整數轉換)。分別怎麼用=delete達成?
(1) 把複製建構子與複製賦值 =delete,並自行提供移動建構子與移動賦值(如 std::thread、std::unique_lock<> 的做法)。
(2) 宣告 void foo(int) = delete; —— 刪除函式仍參與重載決議,foo(42) 會精確匹配到已刪除的 int 版本而編譯失敗;foo((short)42) 則合法。
Question 3 - Lambda 捕獲列表 [recall]
int i=1234, j=5678, k=9;建立auto f=[=,&j,&k]{return i+j+k;};之後執行i=1; j=2; k=3;,此時f()回傳多少?若捕獲改為[&,j,k]又回傳多少?
[=,&j,&k]:i 以複製捕獲(快照 1234),j、k 以引用捕獲(取當前值 2、3)→ 1234+2+3 = 1239。
[&,j,k]:i 以引用捕獲(當前值 1),j、k 以複製捕獲(快照 5678、9)→ 1+5678+9 = 5688。
口訣:複製在建立 lambda 時定值,引用在呼叫時取值。
Question 4 - thread_local 三類變數 [recall]
哪三類變數可以宣告為
thread_local?函式內的 thread_local 變數何時建構?程式從main()返回時,仍在執行的其他執行緒,其 thread_local 解構子會被呼叫嗎?
三類:命名空間作用域變數、類別靜態成員、函式區域變數。前兩類保證在該執行緒「首次使用前」建構(確切時點由實作決定);函式區域的在控制流首次通過宣告時建構,沒呼叫該函式的執行緒不會建構。程式退出時仍在執行的執行緒,其 thread_local 解構子不會被呼叫;正常結束的執行緒則以建構的逆序解構。
Question 5 - 參數包與 sizeof... [recall]
template<typename... Args> unsigned count_args(Args... args){ return sizeof...(Args); }中sizeof...回傳什麼?其結果可否用來指定陣列長度?另外當Params = int, char時,std::tuple<std::unique_ptr<Params>...>展開成什麼?
sizeof...(Args) 回傳參數包中元素的個數,且結果是常數運算式,可以指定陣列長度等。
包展開會把模式逐項套用:std::tuple<std::unique_ptr<int>, std::unique_ptr<char>>。
Question 6 - 四庫對照表 [recall]
附錄 B 的四方對照中:Boost 執行緒庫與 POSIX C 在「原子操作」欄填什麼?Java 的互斥與「等待/通知」分別靠什麼機制?
Boost 與 POSIX C 的原子操作欄皆為 N/A(只有 C++11 的 std::atomic<> 家族與 Java 的 java.util.concurrent 提供)。Java 的互斥用語言內建的 synchronized 區塊;等待/通知用 java.lang.Object 的 wait()/notify(),且必須在 synchronized 區塊內使用。
Question 7 - 撰寫完美轉發包裝 [application]
請寫一個模板函式
relay,能接受任意數量、任意值類別(左值/右值)的引數並原封不動轉交給foo;並說明為什麼不能直接寫foo(args...)。
template<typename... Args>
void relay(Args&&... args)
{
fooforward<Args>(args)...;
}
參數宣告 Args&&... 讓左值把 Args 推導為左值引用、右值推導為一般型別(引用摺疊)。若直接 foo(args...),具名參數 args 在函式內全是左值,右值性遺失,foo 只能複製而無法移動;std::forward<Args> 依推導結果還原原本的值類別。
Question 8 - 為 ATM 新增訊息 [application]
想為 ATM 加一個「列印明細」功能:使用者在選單狀態按下
p時印出一行文字。請描述需要修改哪些地方(訊息 struct、哪個狀態的 handle 鏈、誰負責處理),框架本身需要改嗎?
- 定義空訊息 struct,例如
struct print_receipt {};,再定義給介面的struct display_receipt {};。 - main 的鍵盤迴圈加
case 'p': atmqueue.send(print_receipt());。 - 在
atm::wait_for_action()的incoming.wait()鏈上追加.handle<print_receipt>(...),於 handler 內interface_hardware.send(display_receipt());(狀態可留在wait_for_action)。 interface_machine::run()的鏈上加.handle<display_receipt>(...)印出文字。
框架完全不用改——queue/sender/dispatcher 對訊息型別是泛型的,任何型別都會被wrapped_message<T>包裝。若掛錯狀態,訊息會被 dispatcher 丟棄。
Question 9 - constexpr 與初始化競爭 [analysis]
為什麼標準要求
std::mutex的預設建構子是 constexpr?從初始化時機與競爭條件的角度分析:若它是動態初始化,會出現什麼風險?
constexpr 建構子 + 常數運算式引數 → 物件屬於常數/靜態初始化,保證在任何程式碼執行之前完成。全域互斥鎖的用途正是保護其他變數的同步存取:若其初始化是動態的(main 開始後、與其他初始化交錯執行),多執行緒程式早期就可能發生「鎖還沒建構完就被拿去 lock」——對鎖本身產生初始化競爭,而這把鎖無法保護自己的初始化。靜態初始化一併消除了跨編譯單元的初始化順序問題。std::atomic<> 同理,還需要 trivial 特殊成員(=default)配合。
Question 10 - dispatcher 的解構子設計 [analysis]
訊息框架讓
dispatcher/TemplateDispatcher在解構子中執行wait_and_dispatch(),並以擲出close_queue例外來結束迴圈。分析:為何解構子必須標noexcept(false)?這個設計的優點與代價各是什麼?
noexcept(false) 的必要性:C++11 起解構子預設為 noexcept(true),close_queue 例外若從預設解構子逸出會立即 std::terminate();顯式標記後例外才能傳到 run() 的 catch。
優點:成就 receiver.wait().handle<M1>(f1).handle<M2>(f2); 的流暢鏈式語法——整條鏈都是臨時物件,運算式結束、鏈尾解構時「自動」開始等待;chained 旗標保證只有鏈尾等待,而例外能一次穿出所有層級,乾淨地終止各角色的 for(;;)。
代價:會擲例外的解構子本質危險——若它在堆疊回捲(另一個例外傳播中)期間被解構,程式仍會 terminate;控制流藏在「解構+例外」裡,可讀性與除錯難度較高;未匹配訊息被靜默丟棄也增加誤用風險。
| 關鍵字 | 答案 |
|---|---|
| 具名右值引用參數 | 在函式內是左值;移動需 std::move |
=delete |
編譯期禁用;可刪特定重載擋隱式轉換 |
[=,&j,&k] / [&,j,k] |
複製=建立時定值;引用=呼叫時取值 |
| thread_local | 三類變數;區域者於控制流首過時建構;程式退出時未結束執行緒不解構 |
sizeof...(Args) |
元素個數;常數運算式 |
| 四庫對照 | 原子操作只有 C++11 與 Java 有;Boost/POSIX 為 N/A |
| 完美轉發 | Args&&... + std::forward<Args>(args)... |
| 新增 ATM 訊息 | 定義 struct + 對應狀態掛 .handle<>();框架不用改 |
std::mutex constexpr 建構子 |
靜態初始化,消除鎖自身的初始化競爭 |
| dispatcher 解構子 | 真正的等待點;noexcept(false) 讓 close_queue 穿出 |