Function Objects and the Application Operator
A thread can be given a function and its arguments, and the function then runs in a separate thread with those arguments.
Why a Thread Needs Data
A task that runs in a thread usually has work to do on some data. A thread that sums a list, sorts a collection, or updates a set of values needs access to that data. The source makes the basic point directly: a task needs data to work upon, and that data can be passed to the task as arguments, either as the data itself or as pointers or references to the data.
A thread is created from two things: a function and the arguments that function should receive. When the thread starts, the function runs in a separate thread with those arguments. The rest of this article follows that idea through a small program, tracing where each vector comes from and which copy or reference a thread's function finally works on.
Read the program first and notice three things: which vectors exist in main, which functions run on separate threads, and what each thread is given to work on. The mechanisms the program depends on are explained afterwards.
Tracing a Vector from main into a Thread
- main creates two vectors,
some_vecand vec2, and owns both of them - f takes a vector reference, so its parameter names the caller's object
- std::
ref(some_vec)is the wrapper that gives thread t1 a reference tosome_vec - F{vec2} is a temporary function object built from vec2
The thread t1 is created from the function f and one argument, ref(some_vec). When the thread starts, the effect is that f(some_vec) executes in the new thread. The same idea applies to any thread built from a function and its arguments: the function is the work, and the arguments are the data it operates on.
The thread t2 is given a different kind of object. A temporary F object is built from vec2, and the new thread calls that object's application operator. The object itself is what travels into the thread, and the data it needs travels inside it.
What do you think happens?
Thread t1 runs f, which pushes a value onto v. After t1 has finished, does some_vec in main contain the pushed value?
Reveal answer
Yes. The wrapper gives f a reference to some_vec, so f's push_back changes the same vector that main created.
The reference wrapper is the reason the change reaches the original. Without it, the thread would not be given the original vector to change.
Reference Parameters and the Original Object
A parameter declared with a reference type, such as vector<double>& v, is a name for the caller's object. The source puts it directly: a reference parameter names the caller's object, so the function works on the original data. The parameter is not a separate vector that happens to hold the same values. It is another name for the same vector.
Inside f, the statement v.push_back(1.0) modifies the vector through the name v. Because v names the same object as some_vec, the change is visible through some_vec in main. The name changes, but the object does not change identity. That is why the reference is the key to a thread function that updates data in place.
Function Objects Carry Their Own Data
A function object is a struct or class that overloads operator(), and it can carry data in its members. Calling the object with parentheses runs its operator() member, which is why a function object can be called like a function.
In the program, struct F is a function object. Its constructor takes a vector reference and stores that reference in its member v. The data therefore travels with the object: when F{vec2} is handed to the thread, the thread receives the object, and the object already knows which vector to work on.
- The constructor stores the reference, so the object carries its data
operator()is the application operator that runs when the object is calledobj()callsoperator(), which pushes onto vec2 through the stored reference
Tracing the thread t2
Starting from the program in the earlier section, what happens to vec2 when thread t2 runs?
- 1Build the object
F{vec2} constructs a temporary F whose member v refers to vec2 in main.
- 2Launch the thread
std::thread t2 receives that F object and starts running in a new thread.
- 3Call the application operator
The new thread calls the object's
operator(), which is the call the program describes as F(vec2)() executing in a separate thread. - 4Update through the member
operator()works through the stored reference v, so the update lands in vec2 in main.
Thread t2 changes vec2, the vector created in main, through the reference the function object stored.
Value, std::ref, and Function Objects Side by Side
The three ways of giving data to a thread differ in what the thread receives and therefore in what happens to the original object. The table places them side by side.
| Argument form | What the thread receives | Effect on the original in main |
|---|---|---|
| Passing some_vec directly | A copy made for the thread | Changes made by the thread do not reach some_vec in main |
| Passing std::ref(some_vec) | A reference to some_vec | Changes made through the parameter reach some_vec in main |
| Passing F{vec2} | A function object holding a reference to vec2 | operator() changes vec2 through the stored member v |
How each form of argument affects the original vector
The source states that std::ref is used to pass references to threads. The expression ref(some_vec) is how the first thread receives a reference to some_vec rather than the vector as a plain argument. Because f declares its parameter as a vector reference, the reference that arrives in the thread refers to the same vector main created.
Mistakes When Passing Data to Threads
Common mistake Expecting a thread's function to change the original vector when the vector was passed without std::ref
What it looks likestd::thread t(f, some_vec); with f taking a vector referenceWhy it failsThe thread does not receive a reference to
some_vecin main, so the change is not the original object's change.Do this insteadPass std::ref(some_vec) when the thread's function must update the caller's vector.
Common mistake Assuming that a reference parameter always means a copy was made
What it looks likeReading void f(std::vector<double>& v) and expecting v to be an independent vector
Why it failsA reference parameter names the caller's object, so the function works on the original data.
Do this insteadTreat v as another name for the caller's vector and expect its changes to be visible through the original name.
Common mistake Building a function object that stores a reference to a temporary
What it looks likeA struct that keeps a reference to a vector that is destroyed before the thread runs
Why it failsThe stored reference then refers to an object that no longer exists while the thread still uses it.
Do this insteadStore references only to objects that outlive the thread, such as vectors owned by main until after join.
Common mistake Ending main without joining the threads
Trace the Two Threads
Given a version of the program where f is changed to push 3.0 onto v and main creates only some_vec, with thread t1 built as std::thread t1(f, std::ref(some_vec)): what does some_vec contain after t1.join() returns, and what would change if the call were std::thread t1(f, some_vec) instead?
Hints
- Ask which object v names when the wrapper is std::
ref(some_vec). - Ask whether the thread receives the original vector or a separate copy when the argument is passed directly.
Key Takeaways
- A thread is created from a function and its arguments, and the function runs in the new thread with those arguments.
- A reference parameter such as vector<double>& v names the caller's object, so changes made through it reach the original data.
- A function object is a struct or class that overloads
operator(), and it can carry data in its members, such as a stored vector reference. - std::ref passes a reference to a thread, so the thread-launched function works on the original object rather than a separate copy.