傳遞參數與轉移所有權 (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* 指向區域緩衝區:懸空指標陷阱

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::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::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

只可移動型別

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

std::thread 的所有權轉移

std::threadstd::unique_ptrstd::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::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_ptrstd::ifstream)
解構時自動 join 的標準型別 C++20 std::jthread(書中 joining_thread/scoped_thread 的標準化)
Connected Pages
傳遞參數與轉移所有權 (Passing Arguments and Transferring Ownership)
  • Overview Table
  • 傳遞參數:一律先拷貝
    1. char* 指向區域緩衝區:懸空指標陷阱
    2. 需要參考時:std::ref
    3. 成員函式指標
    4. 只可移動型別
  • std::thread 的所有權轉移
  • 函式傳出與傳入
  • scoped_thread:擁有所有權的 RAII(代碼 2.6)
  • joining_thread(代碼 2.7)≈ C++20 std::jthread
  • 執行緒群組:std::vector(代碼 2.8)
  • Exam/Test Patterns
  • Related Notes