Passing Arguments to Threads

When we create a thread using std::thread in C++, we often need to pass arguments to the function or callable object that the thread will execute. Understanding how to pass these arguments correctly is crucial for ensuring thread safety and avoiding unexpected behavior. Let’s explore the different ways to pass arguments to threads in C++.

1. Passing Arguments by Value

 By default, arguments are passed to the thread function by value. This means that a copy of each argument is made when the thread is created, and the function inside the thread operates on those copies.

#include <iostream>
#include <thread>

void printSum(int a, int b) {
    std::cout << "Sum: " << a + b << std::endl;
}

int main() {
    int x = 10;
    int y = 20;

    // Pass arguments by value (default behavior)
    std::thread t(printSum, x, y);

    t.join();

    return 0;
}

  

In this example, the values of x and y are copied when the thread t is created, so the printSum function inside the thread operates on copies of x and y. This is the simplest and safest way to pass arguments, especially if you don’t need to modify the original variables.

2.Passing Arguments by Reference

Passing arguments by reference allows the thread function to modify the original variables. To pass arguments by reference, you must use std::ref or std::cref to wrap the arguments. This is necessary because std::thread always tries to make a copy of the arguments, and using std::ref signals to pass a reference instead.

#include <iostream>
#include <thread>
#include <vector>
#include <algorithm>
#include <functional>

void modifyVector(std::vector<int>& vec) {
    std::transform(vec.begin(), vec.end(), vec.begin(), [](int n) { return n * 2; });
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // Pass argument by reference using std::ref
    std::thread t(modifyVector, std::ref(numbers));

    t.join();

    // Display modified vector
    for (int n : numbers) {
        std::cout << n << " ";
    }
    std::cout << std::endl;

    return 0;
}

Here, std::ref(numbers) is used to pass the vector numbers by reference. The modifyVector function operates directly on the original vector, doubling each element. Without std::ref, the thread would receive a copy of the vector, and changes would not affect the original.

3. Passing Multiple Arguments

We can can pass multiple arguments to a thread function, mixing different passing methods (by value, by reference, etc.). Each argument must be handled appropriately based on how you want it to be passed.

#include <iostream>
#include <thread>
#include <string>
#include <functional>

void complexFunction(int id, const std::string& name, std::vector<int>& data) {
    std::cout << "Thread " << id << ": Hello, " << name << "!" << std::endl;
    std::cout << "Data size: " << data.size() << std::endl;
}

int main() {
    int id = 1;
    std::string name = "Alice";
    std::vector<int> data = {1, 2, 3, 4, 5};

    // Create a thread passing multiple arguments
    std::thread t(complexFunction, id, std::cref(name), std::ref(data));

    t.join();

    return 0;
}

Here, we pass id by value, name by constant reference using std::cref, and data by reference using std::ref. This allows the thread function to handle each argument as needed, ensuring the correct behavior.

4.Passing Arguments Using Move Semantics

When dealing with large objects or resources that are expensive to copy, you may want to transfer ownership of the object to the thread function using move semantics. This is particularly useful when working with objects like std::unique_ptr that cannot be copied.

#include <iostream>
#include <thread>
#include <memory>

void processUniquePtr(std::unique_ptr<int> ptr) {
    std::cout << "Processing unique_ptr with value: " << *ptr << std::endl;
}

int main() {
    auto myPtr = std::make_unique<int>(42);

    // Pass argument using std::move to transfer ownership
    std::thread t(processUniquePtr, std::move(myPtr));

    t.join();


    if (!myPtr) {
        std::cout << "myPtr is now nullptr after moving." << std::endl;
    }

    return 0;
}

In this example, std::move(myPtr) transfers ownership of the std::unique_ptr to the processUniquePtr function. After the thread has been created, myPtr is set to nullptr, indicating that ownership has been transferred.