Passing Data to Threads
Learn techniques for passing arguments to thread functions safely.
How Do You Pass Data to Threads?
By default, std::thread copies all arguments into the new thread's internal storage. To pass data by reference instead, you must explicitly use std::ref(), because the thread may outlive the variables it references.
When you pass arguments to a thread function, they are copied into the new thread's storage, even if the function takes parameters by reference.
#include <iostream>
#include <thread>
void print_value(int x) {
std::cout << "Value: " << x << '\n';
}
int main() {
int num = 42;
std::thread t(print_value, num); // num is copied
t.join();
}
This copying happens because std::thread stores copies of all arguments internally. The thread function might run long after the original variables go out of scope.
The Reference Problem
Consider this code that tries to modify a value:
void increment(int& x) { // Takes by reference
++x;
}
int main() {
int value = 0;
std::thread t(increment, value); // Passes copy, not reference!
t.join();
std::cout << value << '\n'; // Still 0!
}
Even though increment takes an int&, the thread receives a copy of value. The copy gets incremented, but value in main() remains unchanged.
This is actually a safety feature. If std::thread passed references directly, you could easily create dangling reference bugs:
void dangerous() {
int local = 42;
std::thread t(increment, local); // If this were a reference...
t.detach();
} // local destroyed, but thread still has reference to it!
Using std::ref for True References
If you actually need to pass by reference, use std::ref() (or std::cref() for const references):
#include <iostream>
#include <thread>
#include <functional> // for std::ref
void increment(int& x) {
++x;
}
int main() {
int value = 0;
std::thread t(increment, std::ref(value)); // Explicitly pass reference
t.join();
std::cout << value << '\n'; // Now prints 1
}
std::ref() creates a std::reference_wrapper<T> that is copyable but behaves like a reference. When the thread function receives it, it unwraps back to an actual reference.
When using `std::ref()`, you are responsible for ensuring the referenced object outlives the thread. This is why it requires explicit opt-in.
void dangerous() {
int local = 42;
std::thread t(increment, std::ref(local));
t.detach(); // DANGEROUS: local may be destroyed while thread runs
}
Passing Pointers
Pointers work as you'd expect - the pointer value is copied, but it still points to the original object:
void set_to_zero(int* ptr) {
*ptr = 0;
}
int main() {
int value = 42;
std::thread t(set_to_zero, &value);
t.join();
std::cout << value << '\n'; // Prints 0
}
Pointers have the same lifetime concerns as references - make sure the pointed-to object outlives the thread.
Passing Large Objects
For large objects, copying can be expensive. You have several options:
Option 1: Pass by Pointer or Reference
void process(const std::vector<int>& data) {
// Process data...
}
int main() {
std::vector<int> big_vector(1'000'000);
std::thread t(process, std::cref(big_vector)); // Pass const reference
t.join();
}
Option 2: Move the Object
If you don't need the object after starting the thread, move it:
void process(std::vector<int> data) { // Takes by value
// Process data...
}
int main() {
std::vector<int> big_vector(1'000'000);
std::thread t(process, std::move(big_vector)); // Move into thread
// big_vector is now empty!
t.join();
}
This avoids copying by transferring ownership to the thread.
Option 3: Shared Pointer
For shared ownership between threads:
void process(std::shared_ptr<std::vector<int>> data) {
// Process data...
}
int main() {
auto big_vector = std::make_shared<std::vector<int>>(1'000'000);
std::thread t(process, big_vector); // shared_ptr is copied (cheap)
// Both main and thread can use the data
t.join();
}
Passing Move-Only Types
Some types can only be moved, not copied (like std::unique_ptr). You must use std::move():
void take_ownership(std::unique_ptr<int> ptr) {
std::cout << *ptr << '\n';
}
int main() {
auto ptr = std::make_unique<int>(42);
std::thread t(take_ownership, std::move(ptr)); // Must move
// ptr is now nullptr
t.join();
}
String Literal Gotcha
Be careful with string literals and functions taking std::string:
void print(const std::string& s) {
std::cout << s << '\n';
}
int main() {
const char* text = "Hello";
std::thread t(print, text); // Potential problem!
t.detach();
}
The issue: text is a pointer to a string literal. The pointer is copied into the thread, and conversion to std::string happens inside the thread. If the original pointer becomes invalid before the conversion happens, you get undefined behavior.
Safe version:
int main() {
const char* text = "Hello";
std::thread t(print, std::string(text)); // Convert immediately
t.detach();
}
Or use a std::string from the start:
int main() {
std::string text = "Hello";
std::thread t(print, text); // string is copied
t.detach();
}
Multiple Arguments
Passing multiple arguments is straightforward:
void print_sum(int a, int b, int c) {
std::cout << "Sum: " << (a + b + c) << '\n';
}
int main() {
std::thread t(print_sum, 10, 20, 30);
t.join(); // Prints "Sum: 60"
}
Each argument is copied (or moved if you use std::move()).
Passing to Member Functions
For member functions, the first "argument" must be the object (pointer, reference, or smart pointer):
class Calculator {
int base_;
public:
Calculator(int base) : base_(base) {}
void add(int value) {
std::cout << "Result: " << (base_ + value) << '\n';
}
};
int main() {
Calculator calc(100);
// Pass pointer to object, then the function argument
std::thread t(&Calculator::add, &calc, 42);
t.join(); // Prints "Result: 142"
}
You can also use a copy of the object:
std::thread t(&Calculator::add, calc, 42); // calc is copied
Or move it:
std::thread t(&Calculator::add, std::move(calc), 42); // calc is moved
Using Lambdas for Clarity
Lambdas often make argument passing clearer:
int main() {
int value = 42;
std::vector<int> data = {1, 2, 3, 4, 5};
// Capture by value (copy)
std::thread t1([value, data]() {
std::cout << value << '\n';
// data is a copy
});
// Capture by reference (careful with lifetime!)
std::thread t2([&value, &data]() {
++value;
data.push_back(6);
});
t1.join();
t2.join();
}
For move-only types:
auto ptr = std::make_unique<int>(42);
// C++14 init capture for move
std::thread t([p = std::move(ptr)]() {
std::cout << *p << '\n';
});
t.join();
Summary Table
| Scenario | Method | Example |
|---|---|---|
| Pass by value (copy) | Default | std::thread t(f, x); |
| Pass by reference | std::ref() |
std::thread t(f, std::ref(x)); |
| Pass by const reference | std::cref() |
std::thread t(f, std::cref(x)); |
| Move ownership | std::move() |
std::thread t(f, std::move(x)); |
| Member function | Pointer/reference to object | std::thread t(&C::f, &obj, arg); |
| Lambda capture | Closure | std::thread t([&]() { ... }); |
Best Practices
-
Prefer copying for small, cheap-to-copy types (int, double, small structs)
-
Use
std::ref()only when you need to modify the original, and ensure lifetime safety -
Move large objects when you don't need them after starting the thread
-
Use lambdas when argument passing gets complex - captures make intent clear
-
Convert string literals to
std::stringimmediately before passing to detached threads -
Avoid capturing by reference in lambdas passed to detached threads
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Passing Data to Threads - Quiz
Test your understanding of the lesson.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!