Concepts / Function Objects and the Application Operator

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.

  • Programming

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

cpp
Notes on the code
  • main creates two vectors, some_vec and 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 to some_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.

some_vec in maincreated in mainstd::thread t1constructed from f and oneargumentf parameter vvector referencestd::ref(some_vec)reference wrapperwrappedpassed as argumentruns f with this argument
When some_vec is passed through std::ref, which object does f's parameter end up referring to?

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
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.

some_vecvector created in mainone vector objectboth names refer to itvreference parameter of fnamesrefers to
When f changes v, which object in main is changed?

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

Definition

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.

cpp
Notes on the code
  • The constructor stores the reference, so the object carries its data
  • operator() is the application operator that runs when the object is called
  • obj() calls operator(), which pushes onto vec2 through the stored reference
Worked example

Tracing the thread t2

Starting from the program in the earlier section, what happens to vec2 when thread t2 runs?

  1. 1
    Build the object

    F{vec2} constructs a temporary F whose member v refers to vec2 in main.

  2. 2
    Launch the thread

    std::thread t2 receives that F object and starts running in a new thread.

  3. 3
    Call 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.

  4. 4
    Update through the member

    operator() works through the stored reference v, so the update lands in vec2 in main.

Result

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 formWhat the thread receivesEffect on the original in main
Passing some_vec directlyA copy made for the threadChanges made by the thread do not reach some_vec in main
Passing std::ref(some_vec)A reference to some_vecChanges made through the parameter reach some_vec in main
Passing F{vec2}A function object holding a reference to vec2operator() 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 like
    std::thread t(f, some_vec); with f taking a vector reference
    Why it fails

    The thread does not receive a reference to some_vec in main, so the change is not the original object's change.

    Do this instead

    Pass 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 like

    Reading void f(std::vector<double>& v) and expecting v to be an independent vector

    Why it fails

    A reference parameter names the caller's object, so the function works on the original data.

    Do this instead

    Treat 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 like

    A struct that keeps a reference to a vector that is destroyed before the thread runs

    Why it fails

    The stored reference then refers to an object that no longer exists while the thread still uses it.

    Do this instead

    Store 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

MEDIUM

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.

Related concepts

More concepts starting with F