C++11
std::thread
object references
multithreading
concurrency

Passing object by reference to stdthread in C11

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In C++11, std::thread copies arguments by default, which is safe but often surprising when you expect the worker to update the original object. Passing by reference requires explicit intent with std::ref or std::cref. Correctness also depends on object lifetime and synchronization, because reference passing does not automatically make shared access thread-safe.

Understand the Default Copy Semantics

When you write std::thread(fn, arg), the argument is stored as a decayed value inside thread state. That means your callable often receives a copy, even if arg is an lvalue in the caller.

cpp
1#include <iostream>
2#include <string>
3#include <thread>
4
5void append_suffix(std::string text) {
6    text += "-worker";
7    std::cout << "inside: " << text << '\n';
8}
9
10int main() {
11    std::string value = "main";
12    std::thread t(append_suffix, value);
13    t.join();
14
15    std::cout << "after join: " << value << '\n';
16}

The worker prints modified text, but value in main is unchanged because the thread function operated on a copy.

Pass Writable References with std::ref

To pass the original object, wrap it in std::ref. The callable should accept a reference parameter.

cpp
1#include <functional>
2#include <iostream>
3#include <string>
4#include <thread>
5
6void append_suffix(std::string& text) {
7    text += "-worker";
8}
9
10int main() {
11    std::string value = "main";
12
13    std::thread t(append_suffix, std::ref(value));
14    t.join();
15
16    std::cout << value << '\n';
17}

For read-only access, use std::cref with a const reference parameter.

Lifetime Rules Are Non-Negotiable

If you pass a reference, the referenced object must outlive the thread’s use of it. This is where many subtle crashes come from.

Safe pattern:

  • allocate object in scope that survives thread execution.
  • launch thread with std::ref.
  • 'join before leaving scope.'

Danger pattern:

  • pass reference to stack object.
  • detach thread.
  • function returns while thread still reading or writing object.

Detached threads with references require very careful ownership design and are best avoided unless absolutely necessary.

Shared Mutation Needs Synchronization

Passing by reference only shares identity. It does not prevent races. If two threads write or one writes while another reads, protect with mutexes or atomics.

cpp
1#include <functional>
2#include <iostream>
3#include <mutex>
4#include <thread>
5#include <vector>
6
7void increment_many(int& counter, std::mutex& mtx, int n) {
8    for (int i = 0; i < n; ++i) {
9        std::lock_guard<std::mutex> lock(mtx);
10        ++counter;
11    }
12}
13
14int main() {
15    int counter = 0;
16    std::mutex mtx;
17    std::vector<std::thread> workers;
18
19    for (int i = 0; i < 4; ++i) {
20        workers.emplace_back(increment_many, std::ref(counter), std::ref(mtx), 10000);
21    }
22
23    for (auto& t : workers) {
24        t.join();
25    }
26
27    std::cout << "counter = " << counter << '\n';
28}

Without the mutex this program has undefined behavior.

Passing Class Instances and Member Functions

For member functions, pass the object pointer or reference plus method pointer. If the object should be shared and mutated, use std::ref on the instance.

cpp
1#include <functional>
2#include <iostream>
3#include <thread>
4
5struct Accumulator {
6    int total = 0;
7    void add(int v) { total += v; }
8};
9
10int main() {
11    Accumulator a;
12    std::thread t(&Accumulator::add, std::ref(a), 5);
13    t.join();
14
15    std::cout << a.total << '\n';
16}

This works because std::ref(a) ensures the member function runs on the original object.

References Versus Moves

Not all thread argument issues are about references. For move-only types such as std::unique_ptr, pass ownership with std::move, not std::ref.

Use reference when shared lifetime is external. Use move when the worker should own the resource.

Common Pitfalls

  • Forgetting std::ref and expecting caller object to be modified.
  • Passing references to temporaries or short-lived stack values.
  • Sharing mutable referenced state without mutexes or atomics.
  • Detaching threads that still depend on referenced objects.
  • Confusing reference passing with move semantics for ownership transfer.

Summary

  • 'std::thread copies arguments unless you explicitly request reference semantics.'
  • Use std::ref for writable references and std::cref for read-only references.
  • Ensure referenced objects outlive all thread use.
  • Synchronize shared mutable access to avoid data races.
  • Distinguish clearly between shared references and moved ownership.

Course illustration
Course illustration

All Rights Reserved.