Understanding std::ref and std::cref in C++ Threads
In C++, std::ref and std::cref are utility
functions that wrap objects in std::reference_wrapper to
allow passing references to callable objects and threading utilities
like std::thread and std::jthread. Because
threading constructors copy or move their arguments by default to ensure
thread safety and avoid dangling references, passing variables directly
will not bind to reference parameters in a worker function. This article
explains why the C++ threading mechanism enforces by-value decay, how
std::ref and std::cref bypass this behavior,
and the safety considerations required when using them.
The Default
Argument Passing Behavior in std::thread
When you construct a std::thread, the constructor takes
the callable target and its arguments by forwarding reference
(Args&&...), decaying them using
std::decay_t. This design choice mirrors utilities like
std::bind and std::make_tuple.
The decay ensures that values passed to the thread are copied or moved into internal thread-storage. If a worker function expects a non-const reference parameter:
void increment(int& value) {
++value;
}Attempting to launch a thread by simply passing an integer variable directly will cause a compilation error:
int counter = 0;
std::thread t(increment, counter); // Compilation Error: cannot bind rvalue to non-const lvalue referenceThe std::thread constructor creates a decayed copy of
counter internally. When it attempts to invoke
increment with this temporary copy, the language rules
prevent binding a non-const lvalue reference (int&) to
a temporary value (rvalue), resulting in a compiler error.
The Role of
std::ref and std::cref
To bypass decay and ensure that the worker function operates on the
original object rather than a copy, you must explicitly wrap the
variable using std::ref or std::cref:
std::ref: Generates astd::reference_wrapper<T>, which behaves like a non-const reference (T&).std::cref: Generates astd::reference_wrapper<const T>, which behaves like a constant reference (const T&).
The thread infrastructure copies the lightweight
std::reference_wrapper object instead of the actual data.
Upon executing the target function, the wrapper implicitly converts back
to a reference bound to the original object.
#include <iostream>
#include <thread>
#include <functional>
void modify_data(int& val) {
val += 42;
}
void print_data(const int& val) {
std::cout << "Value: " << val << '\n';
}
int main() {
int data = 10;
// Passing by reference using std::ref
std::thread t1(modify_data, std::ref(data));
t1.join();
// Passing by const reference using std::cref
std::thread t2(print_data, std::cref(data));
t2.join();
return 0;
}Key Use Cases
- Mutating State in Worker Threads: When a thread must update an external accumulator, buffer, or state variable managed by the parent context.
- Preventing Expensive Copies: When passing large
objects (such as complex structs, large containers, or matrices) to
read-only threads where copying overhead is prohibitive,
std::crefallows passing byconst&efficiently. - Working with Non-Copyable Types: When an object
manages exclusive resources (like
std::mutex) and cannot be copied, passing it to a task requires either moving it or usingstd::ref.
Crucial Safety Considerations
While std::ref and std::cref are necessary
for reference forwarding, they introduce concurrency risks:
- Lifetime Management: The programmer is strictly responsible for guaranteeing that the referenced object outlives the spawned thread. If the owning scope exits while the thread is still running, the reference becomes dangling, leading to undefined behavior.
- Data Races: Passing a non-const reference via
std::refallows concurrent mutation. If multiple threads access or modify the underlying object simultaneously, adequate synchronization mechanisms (such asstd::mutexorstd::atomic) must be implemented to prevent race conditions.