智慧指標練習題 (Practice - Smart Pointers)
Related Concepts
- 05-Smart-Pointers/01-Unique-Ptr
- 05-Smart-Pointers/02-Shared-Ptr
- 05-Smart-Pointers/03-Weak-Ptr
- 05-Smart-Pointers/04-Make-Unique-And-Make-Shared
- 05-Smart-Pointers/05-Pimpl-Idiom
| 關鍵字 | 答案 |
|---|---|
複製 std::unique_ptr |
不能編譯——move-only;move 後來源設為 null |
unique_ptr custom deleter |
型別的第二個模板引數;function pointer / 有狀態 functor 會增大物件 |
| 工廠函式回傳型別 | std::unique_ptr(可單向升級為 std::shared_ptr) |
shared_ptr 大小 |
raw pointer 的 2 倍;deleter 不影響其大小 |
同一 raw pointer 建兩個 shared_ptr |
兩個 control block → 重複 delete → UB |
成員函式把 this 交給 shared_ptr |
繼承 std::enable_shared_from_this(CRTP)+ shared_from_this() |
weak_ptr 已 expired 時 lock() |
回傳 null 的 shared_ptr;建構子版本則擲 std::bad_weak_ptr |
weak_ptr 三大場景 |
caching、observer lists、打破 shared_ptr cycles |
f(shared_ptr<W>(new W), g()) |
g() 可插在 new 與建構子之間拋例外 → 洩漏;用 make function |
make_shared 效率 |
單次配置:物件 + control block 同一 chunk |
| make functions 內部轉發 | 用 小括號 (),不是大括號 {} |
Pimpl + unique_ptr 編譯錯誤 |
incomplete type;特殊成員函式移到實作檔(Impl 定義後)定義 |
Question 1 - unique_ptr 的所有權語意 [recall]
同事寫下
auto p2 = p1;(p1是std::unique_ptr<Widget>)想「備份」指標,接著又問你若改成auto p2 = std::move(p1);之後p1會是什麼狀態。
auto p2 = p1; 不能編譯:std::unique_ptr 體現專屬所有權,是 move-only 型別——若允許複製,兩個指標都自認擁有並將銷毀同一資源。
std::move(p1) 則將所有權轉移給 p2,來源 p1 被設為 null。
Question 2 - custom deleter 與 unique_ptr 大小 [recall]
你需要在 delete 前先寫 log,猶豫該用一般函式(function pointer)還是無捕獲 lambda 當
std::unique_ptr的 custom deleter——兩者對指標物件大小的影響為何?
deleter 型別是 std::unique_ptr 型別的一部分(第二個模板引數)。function pointer 會讓大小從一個 word 變兩個 word;無捕獲 lambda(stateless function object)則零額外開銷,故優先用 lambda。
有狀態的 function object 依捕獲的狀態多寡增大;若 deleter 讓 unique_ptr 大得離譜,通常是設計該改了。
Question 3 - 工廠函式的回傳型別設計 [application]
你為
Investment階層(Stock/Bond/RealEstate)設計工廠函式,deleter 會以Investment*刪除物件,且部分呼叫者之後想共享所有權——該回傳哪種智慧指標?基底類別要注意什麼?
回傳 std::unique_ptr<Investment>:工廠無法預知呼叫者要專屬還是共享所有權,unique_ptr 最高效,且可輕鬆單向轉成 std::shared_ptr(std::shared_ptr<Investment> sp = makeInvestment(args);),反向不可行。
deleter 以基底指標刪除衍生類別物件,Investment 必須有 virtual destructor,否則是 undefined behavior。
Question 4 - shared_ptr 的成本清單 [recall]
面試官要你列出
std::shared_ptr相對 raw pointer 的主要成本,並回答「指定 custom deleter 後shared_ptr物件會變大嗎?」
成本:(1) 大小是 raw pointer 的兩倍(物件指標 + control block 指標);(2) control block 需動態配置(make_shared 可併入物件配置);(3) reference count 增減必須是 atomic 操作,比非 atomic 慢。
custom deleter 不會改變 shared_ptr 的大小——deleter 存放在 control block(heap)中,這與 std::unique_ptr(deleter 屬於型別、可能增大物件)恰好相反。
Question 5 - control block 的建立規則 [recall]
快問快答:以下哪些建構方式會建立新的 control block——
std::make_shared、從std::unique_ptr建構、傳入 raw pointer、傳入既有的std::shared_ptr或std::weak_ptr?
會建立:std::make_shared(物件全新製造)、從 unique-ownership 指標(unique_ptr/auto_ptr)建構、傳入 raw pointer。
不建立:傳入 std::shared_ptr 或 std::weak_ptr——沿用它們指向的既有 control block。
推論:同一 raw pointer 建構多個 shared_ptr ⇒ 多個 control block ⇒ 物件被銷毀多次 ⇒ undefined behavior。
Question 6 - emplace_back(this) 的隱藏炸彈 [analysis]
Code review 時你看到
void Widget::process() { processedWidgets.emplace_back(this); }(容器型別為std::vector<std::shared_ptr<Widget>>),程式可編譯也常常正常運作——請分析何時會爆炸、為什麼,以及完整的修正方案。
this 是 raw pointer,容器內就地建構 shared_ptr 時會為 *this 建立新的 control block;若外部已有 shared_ptr 管理此 Widget,就有兩個 reference count,物件會被銷毀兩次 → undefined behavior(外部沒有 shared_ptr 時暫時不爆,故「常常正常運作」)。
修正:Widget 繼承 std::enable_shared_from_this<Widget>(CRTP),改為 emplace_back(shared_from_this())——它查出既有 control block 再建立新 shared_ptr。
前提:呼叫 shared_from_this() 時必須已存在指向該物件的 shared_ptr,否則 UB(通常擲例外);因此慣用作法是建構子設 private、只透過回傳 shared_ptr 的 static factory(如 create)建立物件。
Question 7 - expired 檢查與安全存取 [recall]
為什麼「先呼叫
wpw.expired()確認沒過期、再自行取值」不是安全的存取方式?正確的兩種做法與其 expired 行為各是什麼?
檢查與存取分離會引入 race condition:兩步之間其他執行緒可能銷毀最後一個 shared_ptr,dereference 變成 UB;需要「檢查 + 取得存取權」的原子操作。
做法一:wpw.lock()——expired 時回傳 null 的 shared_ptr;做法二:std::shared_ptr<Widget> spw(wpw)——expired 時擲出 std::bad_weak_ptr。
Question 8 - weak_ptr 的使用場景與樹狀結構 [recall]
說出
std::weak_ptr的三大典型使用場景;另外,在嚴格階層式的樹狀結構中,parent→child 與 child→parent 的連結分別該用什麼指標?
三大場景:caching(快取需偵測項目是否懸空)、observer lists(subject 不控制 observer 生命週期但不可存取已銷毀者)、打破 shared_ptr cycles。
樹狀結構不需要 weak_ptr:parent→child 用 std::unique_ptr(parent 獨占 child),child→parent 用 raw pointer 即可——child 生命週期絕不會長於 parent,不會懸空。
Question 9 - 回指指標選型 [application]
資料結構中 A 與 C 各以
std::shared_ptr共享 B 的所有權,現在 B 需要一個回指 A 的指標——請在 raw pointer、std::shared_ptr、std::weak_ptr三者中選擇並說明另外兩者的缺陷。
選 std::weak_ptr:A 被銷毀後 B 可偵測懸空,且不影響 A 的 reference count、不阻止 A 被銷毀。
raw pointer:A 銷毀後 B 無法偵測懸空,dereference 是 UB;std::shared_ptr:A、B 互指形成 cycle,reference count 永不歸 0,即使外界都無法存取,兩者也永遠洩漏。
Question 10 - make functions 的三大優點 [recall]
主管問你為何 code base 規定優先用
std::make_unique/std::make_shared而非直接new——請列出三個理由,並指出std::make_unique加入標準的版本。
(1) 消除型別重複:auto upw = std::make_unique<Widget>(); 型別只寫一次;(2) 例外安全:避免 new 與 smart pointer 建構子之間插入其他運算導致洩漏;(3) 效率(僅 make_shared/allocate_shared):物件與 control block 一次配置,程式更小更快。
std::make_shared 是 C++11;std::make_unique 遲至 C++14 才加入(C++11 可自寫基本版,但勿放進 namespace std)。
Question 11 - 參數求值順序與資源洩漏 [analysis]
呼叫
processWidgetshared_ptr<Widget>(new Widget, cusDel), computePriority();在某些編譯器下會洩漏記憶體——請分析編譯器允許的執行順序如何導致洩漏、為何此處不能用make_shared補救,以及兼顧例外安全與效能的正確寫法。
編譯器只保證 new Widget 先於 shared_ptr 建構子,computePriority() 可排在兩者之間;若它在此時拋出例外,new 出的 Widget 尚未被任何智慧指標接管 → 洩漏。因需要 custom deleter,make_shared 無法使用(make functions 不接受 deleter)。
正確寫法:獨立語句 std::shared_ptr<Widget> spw(new Widget, cusDel); 再 processWidgetmove(spw), computePriority();——shared_ptr 建構子即使自身拋例外也保證對傳入指標呼叫 deleter。
std::move(spw) 讓 by-value 參數用 move 建構(不動 reference count),避免傳 lvalue 時複製造成的 atomic 遞增,效能等同原本的 rvalue 寫法。
Question 12 - make functions 的括號行為 [recall]
auto upv = std::make_unique<std::vector<int>>(10, 20);建出的 vector 內容是什麼?若真的想要{10, 20}兩個元素該怎麼辦?
10 個元素、每個值都是 20:make functions 內部以小括號 () 完美轉發參數,呼叫非 initializer_list 建構子;braced initializer 無法被完美轉發。
workaround:先 auto initList = { 10, 20 };(auto 推導出 std::initializer_list<int>)再傳入 make function;或直接用 new 搭配大括號。
Question 13 - Pimpl 編譯錯誤修復 [application]
你把
Widget改成 Pimpl(header 內struct Impl; std::unique_ptr<Impl> pImpl;,未宣告解構子),客戶端寫Widget w;時編譯器抱怨「sizeof/deleteapplied to incomplete type」——請解釋原因並給出修法。
原因:compiler 生成的 ~Widget() 是 implicitly inline,會在 header 可見處生成銷毀 pImpl 的程式碼;unique_ptr 的 default deleter 在 delete 前以 static_assert 檢查指向型別必須 complete,而 header 中 Impl 是 incomplete type → 失敗。
修法:header 中只宣告解構子 ~Widget();,在 widget.cpp 中 struct Widget::Impl { … }; 定義之後寫 Widget::~Widget() = default;——即使預設實作完全可用也要這麼做。
注意連鎖效應:宣告解構子會抑制 move operations 生成(Item 17),要 move 支援就同樣「header 宣告、實作檔 = default」;在 header 內直接 = default 是錯的(move assignment 需銷毀舊 pImpl、move ctor 例外路徑也需 complete type)。
Question 14 - Pimpl 換用 shared_ptr 的差異 [recall]
若 Pimpl 的
pImpl改用std::shared_ptr<Impl>,還需要「header 宣告、實作檔定義」特殊成員函式的儀式嗎?為何實務上仍應選std::unique_ptr?
不需要:std::shared_ptr 的 deleter 型別不屬於指標型別,生成特殊成員函式時指向型別不必 complete,一切照常編譯執行(unique_ptr 的 deleter 屬於型別、換得更小更快的執行期結構,代價是需要 complete type)。
但 Widget 與 Widget::Impl 是獨占擁有關係,std::unique_ptr 才是語意正確的工具——不該為了省儀式而改變所有權語意。
| 模式 | 核心原則 |
|---|---|
| 所有權選型 | 預設 unique_ptr(專屬、近乎零成本);確定要共享才用 shared_ptr;「像 shared_ptr 但可懸空」用 weak_ptr;升級單向:unique_ptr → shared_ptr |
| deleter 設計對比 | unique_ptr:deleter 屬於型別、可能增大物件、需 complete type;shared_ptr:deleter 存 control block、大小恆為兩指標 |
| control block 鐵律 | 一物件一 control block;raw pointer(含 this)重複建構 shared_ptr = 多 control block = UB;this 場景用 enable_shared_from_this |
| weak_ptr 存取 | 檢查與存取必須原子:lock()(null)或 shared_ptr 建構子(bad_weak_ptr) |
| make functions | 預設一律用;不能用的場合(custom deleter、braced initializer)→ new 結果立即在獨立語句交給智慧指標,傳遞時 std::move |
| Pimpl 儀式 | unique_ptr pImpl:特殊成員函式 header 宣告、實作檔(Impl 定義後)= default;shared_ptr 免儀式但語意不對 |