傳遞參數與轉移所有權 (Passing Arguments and Transferring Ownership)
Overview Table
| 概念 | 重點 |
|---|---|
| 傳參預設拷貝 | 參數拷貝至新執行緒的記憶體空間;即使函式參數宣告為參考也照樣拷貝 |
char* 懸空陷阱 |
隱式轉 std::string 可能發生在原函式返回之後;傳入前先顯式轉換 |
std::ref |
內部以右值傳遞 → 綁不上非 const 參考(編譯錯);需 std::ref(data) 包裝 |
| 成員函式指標 | std::thread tf, &obj, args...;第二參數為物件指標 |
| 只可移動型別 | std::unique_ptr 等需 std::move();臨時物件自動隱式移動 |
std::thread 所有權 |
可移動、不可拷貝;賦值給「已關聯執行緒」的物件 → std::terminate() |
| RAII 擁有型包裝 | scoped_thread/joining_thread(≈ C++20 std::jthread) |
| 執行緒群組 | std::vector<std::thread> + emplace_back + 逐一 join() |
傳遞參數:一律先拷貝
傳參很簡單——把參數接在 std::thread 建構函式後面即可。但要注意:參數會被拷貝到新執行緒的記憶體空間(如同臨時變數),即使目標函式的參數是參考形式,拷貝照樣發生。
void f(int i, std::string const& s);
std::thread t(f, 3, "hello"); // 建立呼叫 f(3, "hello") 的執行緒
- 這裡傳入的是字串字面值(
char const*),在新執行緒的上下文中才轉換為std::string
char* 指向區域緩衝區:懸空指標陷阱
void oops(int some_param) {
char buffer[1024];
sprintf(buffer, "%i", some_param);
std::thread t(f, 3, buffer); // ✗ 拷貝的是指標!
t.detach(); // oops 可能先返回 → 未定義行為
}
std::thread t(f, 3, std::string(buffer)); // ✓ 傳入前先轉成 std::string
- 無法保證「隱式轉換」與「
std::thread建構函式的拷貝」誰先發生;建構函式可能只拷貝了buffer指標,轉換發生時oops()已返回、緩衝區已銷毀 - 解法:在傳給建構函式之前就完成轉換string(buffer)
需要參考時:std::ref
反向情形(想傳非 const 參考、卻被拷貝整個物件)不會默默發生,而是編譯錯誤:
void update_data_for_widget(widget_id w, widget_data& data);
std::thread t(update_data_for_widget, w, data); // ✗ 編譯錯誤
std::thread t(update_data_for_widget, w, std::ref(data)); // ✓ 真正傳參考
std::thread建構函式無視目標函式簽章,盲目拷貝;內部再把拷貝以右值傳遞(為支援只可移動型別)- 右值綁不上非 const 左值參考 → 編譯期報錯
std::ref()產生參考包裝,使函式收到data本體;std::thread與std::bind在標準庫中採相同機制
用
std::ref 後,呼叫端必須保證被參考物件的生命週期涵蓋執行緒的使用期間(通常靠 join),否則回到懸空參考問題。成員函式指標
class X { public: void do_lengthy_work(int); };
X my_x;
std::thread tdo_lengthy_work, &my_x, 42); // 呼叫 my_x.do_lengthy_work(42
- 第一個參數:成員函式指標
&X::f;第二個參數:物件指標&my_x - 建構函式的第三個參數起,依序對應成員函式的第一、二…個參數
只可移動型別
std::unique_ptr 這類型別只支援移動 (move)、不能拷貝——所有權轉移後原物件留下空指標:
void process_big_objectunique_ptr<big_object>;
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object, std::move(p)); // 命名變數需顯式 std::move
- 所有權先移轉到新執行緒的內部儲存,再傳給
process_big_object - 臨時物件會自動隱式移動;命名變數必須顯式
std::move()
std::thread 的所有權轉移
std::thread 與 std::unique_ptr、std::ifstream 同屬資源占有 (resource-owning) 型別:可移動、不可拷貝。不可拷貝保證同一時間點一個實例只關聯一條執行緒;可移動讓開發者決定誰擁有它。
std::thread t1(some_function); // t1 ── 執行緒A
std::thread t2 = std::move(t1); // A 移交 t2;t1 變空
t1 = std::thread(some_other_function); // 臨時物件 → 隱式移動;t1 ── 執行緒B
std::thread t3; // 預設建構,無關聯
t3 = std::move(t2); // A 移交 t3
t1 = std::move(t3); // ✗ t1 已擁有 B → std::terminate()!
t1(A) ──move──▶ t2(A) t1: 空
t1 = thread(B) (臨時物件,隱式移動)
t3 ◀──move── t2(A) t2: 空
t1(B) ◀──move── t3(A) ✗ t1 已有執行緒 → std::terminate()
不能藉由「賦新值」丟棄一條執行緒:移動賦值的目標若已關聯執行緒,系統直接呼叫
std::terminate()(noexcept,不拋例外)——與解構函式行為一致。函式傳出與傳入
所有權可跨函式邊界移動:
std::thread f() { return std::thread(some_function); } // 傳出(回傳值)
std::thread g() { std::thread t(some_other_function, 42); return t; }
void fthread t); // 傳入(以值接收
fthread(some_function); // 臨時物件,隱式移動
std::thread t(some_function);
fmove(t); // 命名變數,顯式移動
scoped_thread:擁有所有權的 RAII(代碼 2.6)
thread_guard 只持參考;scoped_thread 直接接管 std::thread 的所有權,並把檢查提前到建構期:
class scoped_thread {
std::thread t; // 擁有,非參考
public:
explicit scoped_threadthread t_) : t(std::move(t_) {
if (!t.joinable()) throw std::logic_error("No thread"); // 建構期檢查
}
~scoped_thread() { t.join(); } // 無條件 join
scoped_thread(scoped_thread const&) = delete;
scoped_thread& operator=(scoped_thread const&) = delete;
};
scoped_thread tthread(func(some_local_state)); // 直接傳入,無獨立變數
| 面向 | thread_guard(代碼 2.3) |
scoped_thread(代碼 2.6) |
|---|---|---|
| 持有方式 | std::thread& 參考 |
std::thread 值(擁有所有權) |
| joinable 檢查 | 解構函式中 | 建構函式中,不可匯入即拋 std::logic_error |
| 解構行為 | 若 joinable 才 join | 無條件 join(建構期已保證) |
| 原 thread 物件 | 仍存在、可被誤用 | 已被移走,杜絕誤用 |
joining_thread(代碼 2.7)≈ C++20 std::jthread
C++17 曾提議加入解構時自動 join 的 joining_thread,未達共識而落空;C++20 以 std::jthread 之名延續。自行實作要點:
- 可變參數樣板建構函式:
std::forward<Callable>(func), std::forward<Args>(args)... - 移動賦值前:若自身
joinable()先join()再接收(避免 terminate) - 解構函式:
if (joinable()) join(); - 提供
get_id()、joinable()、join()、detach()、swap()、as_thread()
執行緒群組:std::vector<std::thread>(代碼 2.8)
std::thread 可移動,因此能放入移動敏感的容器,把執行緒當成一個群組管理——邁向自動化管理的第一步,數量可於執行期決定:
std::vector<std::thread> threads;
for (unsigned i = 0; i < 20; ++i)
threads.emplace_back(do_work, i); // 量產執行緒
for (auto& entry : threads)
entry.join(); // 逐一等待結束
此模式適合「分割演算法工作量、結束前全部會合」的場景;執行緒間傳遞結果的方案(future 等)見 04-Synchronizing-Operations/02-Futures-and-Asynchronous-Tasks。
Exam/Test Patterns
| 情境/關鍵字 | 答案 |
|---|---|
| 執行緒內修改參數,呼叫端「看不到變更」或編譯錯誤 | 參數被拷貝且以右值傳遞;要傳參考用 std::ref(data) |
char buffer[N] 傳給執行緒 + detach() |
隱式轉換可能太晚 → 懸空指標;先 std::string(buffer) 再傳 |
| 「用執行緒跑成員函式」 | std::thread tf, &obj, args...,物件指標排第二 |
傳 std::unique_ptr 進執行緒 |
命名變數需 std::move();臨時物件自動隱式移動 |
t1 = std::move(t3) 但 t1 已關聯執行緒 |
std::terminate()——不能用賦值丟棄執行緒 |
「std::thread 能不能拷貝?」 |
可移動、不可拷貝(同 std::unique_ptr、std::ifstream) |
| 解構時自動 join 的標準型別 | C++20 std::jthread(書中 joining_thread/scoped_thread 的標準化) |