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.

Warning
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

  1. Prefer copying for small, cheap-to-copy types (int, double, small structs)

  2. Use std::ref() only when you need to modify the original, and ensure lifetime safety

  3. Move large objects when you don't need them after starting the thread

  4. Use lambdas when argument passing gets complex - captures make intent clear

  5. Convert string literals to std::string immediately before passing to detached threads

  6. Avoid capturing by reference in lambdas passed to detached threads