Given a piece of work, what are the concrete ways to hand it to a new thread, and which form fits which situation? This chapter covers function pointers, lambdas, member functions, functors, std::bind, move-only callables, the cost of spawning a thread, and how hardware_concurrency() informs how many to start.
std::thread's constructor takes a callable as its first argument, followed by zero or more arguments that get forwarded to that callable when the new thread starts. The constructor signature is roughly template<class F, class... Args> std::thread(F&& f, Args&&... args);. Four shapes show up most often:
| Form | When to pick it |
|---|---|
| Free function pointer | The work is already a standalone function. |
| Lambda | The work is short or needs to capture local state. |
| Member function + object | The work belongs to an object's interface. |
Functor (object with operator()) | The work has state that lives across calls. |
The sections below look at the patterns that come up when picking between these forms, plus a few less common variants like std::bind and move-only callables.
The diagram shows the five callable shapes that fit into the same constructor slot. Each ends up doing the same thing at runtime: storing a copy of the callable plus the arguments, then invoking the callable on the new OS thread.
The most direct form. Hand std::thread the function name and any arguments to forward.
Output:
The function name decays to a function pointer when passed to the constructor. The trailing arguments (2048, "Mumbai-3") get copied into the thread's internal storage, then forwarded to shipOrder once it starts.
Overloaded functions break this pattern. If two functions share a name, the compiler can't tell which overload you mean from the bare function name, and you have to disambiguate with a cast:
That syntax is ugly, which is one reason a lambda is the cleaner choice when overloads are involved.
Quick Check: What's wrong with this constructor call?
<details> <summary>Answer</summary>
The constructor copies count into the thread's internal storage, then tries to bind process's int& parameter to that copy. The standard library refuses (a non-const reference can't bind to an rvalue), so this fails to compile with a long template error. Fix it with std::ref(count).
</details>
Lambdas (since C++11) are the most flexible callable shape. They can capture local variables, define the work inline, and avoid naming small one-off functions.
Output:
The capture list [customer, orderId] copies both variables into the lambda. The new thread owns those copies; the originals in main are untouched.
Capturing by reference is supported but dangerous when the thread might outlive the captured variable:
customer lives on bad's stack frame. The moment bad() returns, that stack frame is gone and the reference inside the lambda dangles. If the thread runs after the return, the read is undefined behavior. Capture by value, or make sure the captured object outlives the thread (a member of a long-lived object, for example).
A common pattern is using a lambda to adapt an awkward signature, especially with overloaded functions or with arguments that need transformation before the call:
Inside the lambda there's room to do setup, error handling, or transformation that would be clumsy as bare constructor arguments.
To run a method on a new thread, pass a pointer-to-member-function as the callable and an object (or pointer to one) as the second argument. The second argument becomes the this pointer inside the method.
Output:
The address-of syntax &InvoiceMailer::send is required. Without the &, the expression is invalid (member-function names don't decay to pointers the way free-function names do). The second argument &mailer is the this pointer the method runs against.
Three variants of the object argument are accepted:
| Form | Effect on the object |
|---|---|
&mailer (pointer) | The thread uses the same object. Caller must keep it alive. |
mailer (value) | The thread gets its own copy. Original is untouched. |
std::ref(mailer) | Same as pointer: same object, reference wrapper handles it. |
Passing by value copies the object into the thread's storage:
For const methods the same syntax works:
The const qualifier on get doesn't change the constructor call, but the compiler enforces that nothing inside get modifies *this.
Quick Check: What does this code print?
<details> <summary>Answer</summary>
0. The constructor copies c into the thread's storage and runs bump on that copy. The c in main is never touched. To modify the original, pass &c or std::ref(c) instead.
</details>
A functor is a class with operator() defined. The instance acts as a callable and can also hold state across calls.
Output:
The functor's state (the discount rate) lives in the object itself, so the same instance can be reused across many calls. Inside std::thread's storage, the functor is copied (or moved) along with its state.
One trap to watch for is the most vexing parse. Constructing a thread from a temporary functor using parentheses parses as a function declaration:
Brace initialization avoids the problem:
For functors with no constructor arguments, the brace form is the only readable option.
std::bind (in <functional>, since C++11) constructs a callable by binding some arguments to a function up front. The result can be passed to std::thread as a single argument with no extras.
Output:
std::bind(sendReminder, 2048, 3) returns a callable that, when invoked with no arguments, calls sendReminder(2048, 3). That callable is then handed to std::thread.
Modern C++ rarely uses std::bind because lambdas do the same job more clearly and with better compiler diagnostics:
The lambda version reads more naturally and produces sharper errors when something is wrong. std::bind still shows up in older codebases and in a few niche cases (placeholders for partial application), but for new thread-spawning code, a lambda is the cleaner choice.
The compiler's error messages for std::bind are notoriously verbose, often spanning hundreds of lines of template instantiation. Lambdas usually produce errors that fit on one screen.
Some callables can be moved but not copied. The most common example is a lambda that captures a std::unique_ptr. std::thread's constructor handles this case correctly, as long as the callable is passed as an rvalue.
Output:
The capture [r = std::move(receipt)] (an init-capture, since C++14) moves the unique_ptr into the lambda. The lambda itself is move-only at that point, because it owns a move-only member. Passing it directly as a temporary to std::thread works because the constructor accepts the callable by forwarding reference and moves it into storage.
If the callable is stored in a variable first, hand it over with std::move:
The same applies to move-only function objects defined as classes. As long as you move them in, std::thread can store and invoke them.
A reminder on argument forwarding for the creation-pattern angle: the constructor copies (or moves) each trailing argument into the thread's storage before invoking the callable. To pass by reference, wrap with std::ref or std::cref. To move a value, use std::move.
Output:
Without std::ref(notes), the code would fail to compile because the thread's internal copy of notes is an rvalue that can't bind to std::string&.
This rule applies to every creation form: free functions, member functions, functors, the result of std::bind. The constructor doesn't know or care which form it's calling; it always copies the arguments first.
Quick Check: Which of these spawns a thread that modifies the original total?
std::thread t(addToTotal, total, 50);std::thread t(addToTotal, &total, 50);std::thread t(addToTotal, std::ref(total), 50);Assume addToTotal is declared as void addToTotal(double& running, double amount);.
<details> <summary>Answer</summary>
C. Option A fails to compile (copy can't bind to non-const reference). Option B passes a double*, which doesn't match the double& parameter signature. Only std::ref(total) produces a reference_wrapper<double> that decays to double& when the function is called.
</details>
Spawning an OS thread isn't free. The kernel allocates a stack (default around 8 MB virtual memory on Linux x86-64, 1 MB on Windows), initializes thread-local storage, registers the thread with the scheduler, and runs setup code. Typical end-to-end cost on modern Linux is 10 to 100 microseconds per thread, depending on hardware and kernel configuration.
Output (varies by machine):
Twenty-seven microseconds per thread is fast in absolute terms but slow compared to a function call (a few nanoseconds). For a task that takes 10 microseconds to do, spawning a thread to run it makes the program slower, not faster.
When a small unit of work is slower to spawn than to run, threading adds overhead instead of saving time. The fix is to either give each thread more work (one thread processing many items, not one thread per item) or to use a thread pool that reuses threads across tasks. std::async is one standard-library option, and custom thread pools cover the rest.
std::thread::hardware_concurrency() returns the runtime's best guess at the number of threads that can execute concurrently, usually the count of logical cores. It returns unsigned int, and the standard explicitly allows 0 when the implementation can't figure it out.
For CPU-bound work, the standard advice is to spawn roughly hardware_concurrency() threads, or one or two fewer to leave a core for the OS and I/O. For I/O-bound work (network requests, disk reads), many more threads can run profitably because each one spends most of its time blocked.
A worked example: split a price list across N worker threads, where N comes from the runtime.
Output (4-core machine):
The work is divided into n chunks. Each worker computes its chunk's sum into its own slot in partials (no shared writes), and main adds the partials at the end. std::cref(prices) shares the input list by reference; std::ref(partials[i]) shares one slot of the output.
Cost: Each emplace_back here constructs a std::thread, which spawns an OS thread (typically 10-100 microseconds). For a list of 100,000 doubles, the per-thread setup is dwarfed by the work. For a list of 100 doubles, the setup dominates and the threaded version is slower than a single-threaded loop.
Spawning more threads than cores is called oversubscription. For CPU-bound workloads, oversubscription generally hurts: the OS scheduler has to context-switch between threads, and each switch costs a few microseconds plus cache disruption. Two threads competing for one core do roughly the same total work as one thread, but with extra scheduling overhead added.
For I/O-bound workloads, oversubscription is normal and expected. A thread blocked on a network read is consuming no CPU; spawning a hundred such threads on an eight-core machine is fine because only the threads that are actually running compete for cores. Web servers and database client pools routinely use far more threads than cores for this reason.
A short demonstration of CPU-bound oversubscription:
Output (8-core machine):
Quadrupling the thread count made the program slower, because the work is CPU-bound and the extra threads just shuffled the same work around with added context-switch overhead. The exact numbers vary by hardware, but the shape of the result is consistent across machines for CPU-bound work.
Quick Check: A program does 1,000 network requests, each taking 100 ms to complete. The machine has 8 cores. Roughly how many threads should it spawn for best total time?
std::async with a thread pool<details> <summary>Answer</summary>
C is fine in principle (each thread spends 100 ms blocked, so the cores stay free). In practice, 1,000 OS threads is wasteful in memory; a thread pool of 100-200 threads (or async I/O with std::async) is a common answer. I/O-bound workloads benefit from many more threads than cores.
</details>
A short decision guide, since the four shapes overlap:
| Situation | Best form |
|---|---|
| Plain existing function with the right signature | Free function pointer |
| Work fits in a few lines, captures local state | Lambda |
| Work is a method on an existing object | Member function + object pointer |
| Work has its own state that lives across runs | Functor |
| Need to bind some arguments and use the result as a callable | Lambda (preferred) or std::bind |
Moving a unique_ptr or other move-only state into the thread | Lambda with init-capture and std::move |
If you're choosing between two forms and they're equally clean at the call site, prefer the lambda. It produces clearer error messages and avoids the most-vexing-parse pitfall of functors and the syntactic noise of pointer-to-member.
Q1: Why does `std::thread t(myFunctor());` fail to spawn a thread, and how do you fix it?
The line parses as a function declaration: t is a function returning std::thread, taking one parameter of type "pointer to a function returning myFunctor". This is the "most vexing parse" in C++. Fix it with brace initialization (std::thread t{myFunctor()};), extra parentheses (std::thread t((myFunctor()));), or by naming the functor first (myFunctor f; std::thread t(f);).
Q2: When you pass an `int&` parameter to `std::thread`, why does the constructor copy refuse to bind to the reference, and what's the fix?
The constructor stores arguments by value in the thread's internal storage. The stored copy is an rvalue, and the C++ language disallows binding a non-const lvalue reference (int&) to an rvalue. The standard library deliberately enforces this to prevent referencing the wrong object without a warning. The fix is std::ref(x) (or std::cref(x) for const references), which wraps the argument in a reference_wrapper that decays back to a real reference when the function is invoked.
Q3: Compare creating a thread with a lambda capturing a `unique_ptr` versus passing the `unique_ptr` as a constructor argument.
Both work, but the lambda-capture form is more flexible. Capturing via [ptr = std::move(uniq)] moves the unique_ptr into the lambda, which then owns it for the thread's lifetime. Passing as a constructor argument (std::thread t(fn, std::move(uniq))) also works, but requires fn to take the unique_ptr by value or rvalue reference, and there's no place to do setup. The lambda form is more common in modern code.
Q4: What does `std::thread::hardware_concurrency()` actually return, and what do you do if it returns 0?
It returns an unsigned int representing the implementation's best guess at the number of threads that can execute in parallel, usually the count of logical cores. The standard explicitly allows it to return 0 when the implementation can't determine a meaningful answer (some embedded systems, virtualized environments, sandboxed runtimes). Production code should treat 0 as "unknown" and fall back to a sensible default like 4 or a configuration-driven value.
Q5: Why might spawning 32 threads on an 8-core machine make a CPU-bound program slower, but speed up an I/O-bound one?
CPU-bound work needs cores. Eight cores can run eight threads at full speed; the other 24 threads sit waiting for a turn while the OS context-switches between them, and each switch costs a few microseconds plus loses cache locality. For I/O-bound work, each thread spends most of its time blocked on disk or network, consuming no CPU. While one thread waits, another can run, so more threads means more concurrent I/O in flight and lower total time.
Exercise 1: Write a program that spawns three threads using three different callable forms: one free function, one lambda, and one functor. Each thread should print "Hello from form X" with its respective label.
Expected Output (order may vary):
<details> <summary>Solution</summary>
</details>
Exercise 2: A class Cart has a method addItem(const std::string& name, int quantity). Write a program that creates a Cart instance and spawns a thread that calls addItem("notebook", 3) on it. Print the cart contents afterward.
Expected Output:
<details> <summary>Solution</summary>
</details>
Exercise 3: Fix this code so the thread actually modifies the caller's total.
Expected Output:
<details> <summary>Solution</summary>
The argument needs to be wrapped in std::ref so the thread holds a reference to the original.
</details>
Exercise 4: Write a program that captures a std::unique_ptr<int> (holding the value 42) into a thread via a lambda, prints the value from inside the thread, and confirms the original pointer is empty after the move.
Expected Output:
<details> <summary>Solution</summary>
</details>
Exercise 5: Predict the output:
Expected Output:
<details> <summary>Solution</summary>
t1 modifies the original c (passed by address), adding 5. t2 modifies a copy of c (passed by value), and that copy is discarded when the thread ends. Only the +5 survives, so the final value is 5.
</details>
Exercise 6: Write a program that uses std::thread::hardware_concurrency() to decide how many threads to spawn, falling back to 4 if the runtime returns 0. Each thread should print its index and the total count.
Expected Output (4-core machine, order may vary):
<details> <summary>Solution</summary>
</details>
Exercise 7: What's wrong with this code, and what's the fix?
<details> <summary>Solution</summary>
The lambda captures body by reference. body is a function parameter that lives on launchEmail's stack frame, and that frame is destroyed when launchEmail returns. The returned thread holds a dangling reference, and reading body inside the thread is undefined behavior.
Fix: Capture by value (or move) so the lambda owns its own copy.
</details>
Exercise 8: Modify the price-sum example from this chapter to use a lambda instead of sumChunk as a free function. The thread should compute the chunk sum and write it into the partials slot via reference capture.
Expected Output (varies based on input size):
<details> <summary>Solution</summary>
</details>
10 quizzes