Starting Threads with Functions
A function object is a struct or class that works like a function because it overloads the application operator, operator().
A Belief That Feels Safe
Imagine you have some data and you want a worker thread to process it. A natural first thought goes like this: if I give the worker a reference to my data, the worker sees my data, and that is all there is to it. Nothing is copied, so nothing is lost, and nothing can go wrong. This belief feels reasonable, and it is exactly the belief this article is designed to correct.
What do you think happens?
Before reading on, commit to a guess: if the main thread and a worker thread both reach the same vector, what decides which one sees which value at any given moment?
Reveal answer
The timing of the two threads, since nothing orders their accesses
When two tasks share one object and nothing fixes which one acts first, the result depends on timing. That is the core of a data race concern, and the rest of this article traces why.
Giving a Struct the Ability to Be Called
The usual way to think about a function is that you give it arguments and it returns a result. A function object reverses the emphasis a little. The object is a value that carries its own data, and calling it runs the code you defined inside its operator(). Because the object can hold data between its creation and its call, it can remember things that a plain function would need to receive again each time.
A function object is a struct or class that works like a function because it overloads the application operator, operator().
This ability to carry data is what makes function objects useful for threads. The source states that a function object can be used to execute code in a separate thread, and that a function object can store references to variables.
Anatomy of a Function Object
A compact struct shows all the pieces a function object needs. Read it as three jobs: a member that holds a reference, a constructor that stores the reference, and the operator that does the work when the object is called. The short illustrative struct below uses those three jobs. It is a teaching sketch written for this article, not the source program itself.
vec2 now contains one element, 1- The member v is a reference, so it does not own a copy of the vector
- The constructor fills the member when the object is created
- The line
obj()works because the struct overloadsoperator()
The operator() declaration is the part that turns F into something you can call. The body of that operator is defined elsewhere in the source program. What matters is the shape: the constructor fills the member, and the operator uses it. When code writes obj(), the compiler calls the struct's operator(), which then works on the data the object already carries.
Tracing the Two Threads in the Source Program
The source program contains both kinds of task side by side. A regular function f takes a reference to a vector as a parameter. A function object F keeps a reference to a vector as a member. Both can do work on the same data, but they receive that data at different moments and in different ways.
Now follow the data through main. Main creates two vectors, some_vec and vec2, and then starts two threads. The first thread runs the regular function f with some_vec, passed by reference. The second thread is started with F{vec2}, a temporary function object whose member refers to vec2. The source comments state that F(vec2)() executes in a separate thread, and that f(some_vec) executes in a separate thread. Each thread is joined back into main before the program finishes.
Read the diagram as a shared path. The function object inside the worker thread leads back to the same vec2 that main still owns. The diagram shows a single shared object reached by two paths, one from main and one from the function object. Nothing in the picture says which path acts first. That missing ordering information is the core of the data race concern.
When the Reference Is Used Later
The reference is captured when the function object is built, but the data is not read at that moment. The worker reads and writes through the member only when operator() runs inside the thread. Between those two points, main may still be running, and the vector may be changed or otherwise touched. The timeline below lays out that gap.
What do you think happens?
If main changes vec2 after the thread has been started, can the worker's function object see that change?
Reveal answer
Yes, the function object refers to the same vec2, so it sees whatever value vec2 holds when the worker reads it
Because the member is a reference, there is no separate copy to protect. Whether the worker sees the old or new contents depends on timing, which is exactly why the shared access is risky.
Why a Shared Reference Creates Data Races
Passing an argument by reference gives a thread access to the original data. Once a task holds a reference to a vector, that task and whoever else holds the same vector are looking at one shared object. If two tasks touch it at the same time, the result depends on timing. This is a data race. The source is clear that the cure is not a cleverer reference. The cure is a different decision about what gets passed.
Reference passing: worker holds a reference to vec2 and main also holds vec2
One shared vec2, reached by main and the worker, so accesses can collide
Value passing: worker holds its own copy of the vector, separate from main's vec2
Two separate objects, one per task, so the worker does not share main's data
The before and after views make the design choice concrete. Passing by reference lets a task use the original data, and it introduces the risk that another task reaches that data at the same time. Passing by value means the task receives its own version of the data. The source states that this eliminates the risk of data races.
Mistakes Learners Make with Thread Arguments
Common mistake Assuming a reference is harmless because nothing is copied
Why it failsNot copying means the data is shared, and shared data touched by two tasks at once depends on timing.
Do this insteadTreat a reference as a shared path to the original object and check who else can reach it.
Common mistake Believing the fix is a cleverer reference
Why it failsThe source says the cure is a different decision about what gets passed, not a different way of writing the reference.
Do this insteadDecide whether the task should receive its own version of the data, and pass by value when it should.
Common mistake Thinking the function object reads the data when it is created
Why it failsThe constructor only stores the reference. The data is accessed later, when
operator()runs.Do this insteadSeparate capture time from use time, and ask what may change between them.
Checking Your Understanding
A worker thread receives a function object F that holds a reference to a vector called data owned by main. Main keeps modifying data while the worker runs. Explain in your own words why this is a data race risk, and state one change to how the argument is passed that removes the risk.
Hints
- Ask who else can reach the same object at the same time
- The source says passing by value gives each task its own data
Key Takeaways
- A function object is a struct or class that works like a function because it overloads
operator(), so an object written asobj(args)calls that operator. - A function object can keep a reference to a variable in a member, set by its constructor, and use that reference later when the thread runs
operator(). - Passing by reference gives a thread the original data, which is shared with other tasks and can cause data races when they touch it at the same time.
- Passing by value gives each task its own data, and the source states that this eliminates the risk of data races.