C++
multithreading
std::thread
function arguments
concurrency

Pass multiple arguments into stdthread

Master System Design with Codemia

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

Introduction

std::thread in C++ accepts a callable (function, lambda, or functor) followed by any number of arguments that get forwarded to it. To pass multiple arguments, list them after the callable: std::thread(func, arg1, arg2, arg3). Arguments are copied by default. To pass by reference, wrap the argument in std::ref(). To pass a member function, provide a pointer to the member function followed by the object pointer and the remaining arguments. Understanding these mechanics is essential for avoiding dangling references, data races, and compilation errors in multithreaded C++ code.

Basic Multiple Arguments

cpp
1#include <iostream>
2#include <thread>
3#include <string>
4
5void process(int id, const std::string& name, double score) {
6    std::cout << "ID: " << id << ", Name: " << name
7              << ", Score: " << score << std::endl;
8}
9
10int main() {
11    // Pass three arguments to process()
12    std::thread t(process, 42, "Alice", 95.5);
13    t.join();
14    return 0;
15}
16// Output: ID: 42, Name: Alice, Score: 95.5

Arguments are forwarded by value — std::thread copies each argument into internal storage. The function receives copies, not the original variables.

Passing by Reference with std::ref

By default, std::thread copies all arguments. To pass by reference, use std::ref():

cpp
1#include <iostream>
2#include <thread>
3#include <functional>
4
5void increment(int& value, int amount) {
6    value += amount;
7}
8
9int main() {
10    int counter = 0;
11
12    // WRONG — does not compile, std::thread copies arguments
13    // std::thread t(increment, counter, 10);
14
15    // CORRECT — use std::ref to pass by reference
16    std::thread t(increment, std::ref(counter), 10);
17    t.join();
18
19    std::cout << "Counter: " << counter << std::endl;  // Counter: 10
20    return 0;
21}

Why std::ref is Needed

std::thread internally uses std::decay to remove references from argument types, then stores copies. std::ref() wraps the variable in a std::reference_wrapper that is copyable but dereferences to the original object.

cpp
1#include <thread>
2#include <vector>
3#include <functional>
4
5void fill_vector(std::vector<int>& vec, int start, int count) {
6    for (int i = 0; i < count; ++i) {
7        vec.push_back(start + i);
8    }
9}
10
11int main() {
12    std::vector<int> data;
13
14    std::thread t(fill_vector, std::ref(data), 100, 5);
15    t.join();
16
17    // data contains: [100, 101, 102, 103, 104]
18    return 0;
19}

Member Functions

To call a member function on a thread, pass a pointer to the member function, followed by the object (pointer or reference), then the remaining arguments:

cpp
1#include <iostream>
2#include <thread>
3
4class Worker {
5public:
6    void process(int id, const std::string& task) {
7        std::cout << "Worker processing task " << id
8                  << ": " << task << std::endl;
9    }
10};
11
12int main() {
13    Worker worker;
14
15    // Pass member function pointer + object pointer + arguments
16    std::thread t(&Worker::process, &worker, 1, "compile");
17    t.join();
18
19    // With shared_ptr
20    auto shared_worker = std::make_shared<Worker>();
21    std::thread t2(&Worker::process, shared_worker, 2, "link");
22    t2.join();
23
24    return 0;
25}

Lambda Expressions

Lambdas are the most flexible way to pass multiple arguments, since captures handle references naturally:

cpp
1#include <iostream>
2#include <thread>
3#include <vector>
4
5int main() {
6    int x = 10;
7    double y = 3.14;
8    std::string name = "test";
9
10    // Capture by value
11    std::thread t1([x, y, name]() {
12        std::cout << x << " " << y << " " << name << std::endl;
13    });
14    t1.join();
15
16    // Capture by reference
17    std::vector<int> results;
18    std::thread t2([&results, x]() {
19        results.push_back(x * 2);
20        results.push_back(x * 3);
21    });
22    t2.join();
23    // results: [20, 30]
24
25    // Lambda with parameters
26    std::thread t3([](int a, int b) {
27        std::cout << "Sum: " << (a + b) << std::endl;
28    }, 5, 10);
29    t3.join();
30
31    return 0;
32}

Move-Only Types

Types like std::unique_ptr cannot be copied but can be moved into a thread:

cpp
1#include <iostream>
2#include <thread>
3#include <memory>
4
5void consume(std::unique_ptr<int> ptr, int multiplier) {
6    std::cout << "Value: " << (*ptr * multiplier) << std::endl;
7}
8
9int main() {
10    auto ptr = std::make_unique<int>(42);
11
12    // Must use std::move for move-only types
13    std::thread t(consume, std::move(ptr), 2);
14    t.join();
15    // Output: Value: 84
16    // ptr is now nullptr
17
18    return 0;
19}

Functors (Function Objects)

cpp
1#include <iostream>
2#include <thread>
3
4struct Multiplier {
5    void operator()(int a, int b) const {
6        std::cout << "Product: " << (a * b) << std::endl;
7    }
8};
9
10int main() {
11    Multiplier mult;
12
13    // Pass functor + arguments
14    std::thread t(mult, 6, 7);
15    t.join();
16    // Output: Product: 42
17
18    // Inline temporary (use extra parentheses to avoid most-vexing parse)
19    std::thread t2((Multiplier()), 3, 4);
20    t2.join();
21
22    return 0;
23}

Common Pitfalls

  • Forgetting std::ref() for reference parameters: std::thread copies all arguments by default. Passing a variable to a function expecting a reference compiles (the copy binds to the reference) but the original variable is not modified. Use std::ref() when you need the thread to modify the caller's data.
  • Dangling references from local variables: If you pass a reference to a local variable using std::ref() and the thread outlives the scope, the reference dangles. Ensure the referenced variable's lifetime extends past thread.join().
  • Forgetting to call join() or detach(): If a std::thread object is destroyed while still joinable, std::terminate() is called. Always call join() (to wait) or detach() (to let it run independently) before the thread object goes out of scope.
  • Passing string literals to std::string parameters: std::thread(func, "hello") copies the const char* pointer, then constructs the std::string inside the thread. If the calling thread exits before construction, the pointer may dangle. Pass std::string("hello") explicitly to ensure the string is copied immediately.
  • Data races when multiple threads share references: Passing the same variable via std::ref() to multiple threads without synchronization (mutex, atomic) causes undefined behavior. Protect shared data with std::mutex or use std::atomic for simple types.

Summary

  • Pass multiple arguments after the callable: std::thread(func, arg1, arg2, arg3)
  • Arguments are copied by default — use std::ref() to pass by reference
  • For member functions, pass &Class::method, then the object pointer, then additional arguments
  • Use std::move() for move-only types like std::unique_ptr
  • Lambdas with captures are the most flexible approach and avoid many argument-passing pitfalls

Course illustration
Course illustration

All Rights Reserved.