C++
multithreading
std::thread
function arguments
concurrency

Pass multiple arguments into stdthread

Interview Questions practice on Codemia

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

Browse interview questions

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

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.