C++
std::thread
concurrency
pass-by-reference
thread-safety

Is it safe to pass arguments by reference into a stdthread function?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Passing arguments by reference into a std::thread function can be safe, but only if lifetime and synchronization are both correct. The language gives you the mechanism, not the guarantees. In real code, the question is not “can I pass by reference,” but “will the referenced object still exist, and will concurrent access be race-free.”

std::thread Copies Arguments by Default

By default, std::thread stores copies of the arguments you pass to it. That protects you from some lifetime bugs, but it also means the thread function does not automatically modify the original object.

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

value in main remains unchanged because the thread receives a copy.

Pass by Reference Explicitly with std::ref

If you want the thread to operate on the original object, use std::ref.

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    std::thread t(append_suffix, std::ref(value));
13    t.join();
14    std::cout << value << '\n';
15}

This is safe only because value remains alive until after join().

The Two Real Safety Conditions

Passing by reference is safe only when both of these are true:

  1. The referenced object outlives all thread access.
  2. Access is synchronized if at least one thread writes.

If either condition fails, the program has undefined behavior.

Lifetime Safety

The most common bug is passing a reference to an object that goes out of scope before the worker finishes. This is especially dangerous with detached threads.

Unsafe idea:

  • create local variable.
  • launch detached thread with std::ref(local).
  • return from the function.

The thread then holds a dangling reference. In most codebases, detaching threads with referenced stack objects is an architectural mistake.

Synchronization Safety

Even if lifetime is correct, simultaneous access can still be unsafe. If multiple threads read and write the same referenced object, you need a mutex or atomic design.

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 << '\n';
28}

Without the mutex, this code has a data race.

Passing by const Reference

If the thread only needs read access, use std::cref and a const reference parameter. This does not remove lifetime requirements, but it reduces accidental mutation.

cpp
1#include <functional>
2#include <iostream>
3#include <string>
4#include <thread>
5
6void print_text(const std::string& text) {
7    std::cout << text << '\n';
8}
9
10int main() {
11    const std::string value = "read-only";
12    std::thread t(print_text, std::cref(value));
13    t.join();
14}

When Reference Passing Is the Wrong Tool

Sometimes ownership transfer is clearer than shared reference access. For example, if a worker should exclusively own a resource, pass a std::unique_ptr with std::move instead of sharing by reference.

Reference passing is best when:

  • one thread owns the object lifetime.
  • sharing is intentional.
  • synchronization policy is clear.

Common Pitfalls

  • Forgetting that std::thread copies arguments unless std::ref is used.
  • Passing references to stack objects that go out of scope too early.
  • Assuming reference passing is safe without locks or atomics.
  • Detaching threads that still depend on referenced objects.
  • Using shared references when ownership transfer would be simpler.

Summary

  • Passing by reference into std::thread can be safe, but only under strict conditions.
  • Use std::ref or std::cref when you truly need reference semantics.
  • Ensure the referenced object outlives the worker thread.
  • Synchronize shared mutable access to avoid data races.
  • Prefer clearer ownership models when shared reference access is unnecessary.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.