std::ref and std::reference_wrapper: common use cases

reference_wrapper provides reference semantics along with the resilience to rebind to a different object.

Tutorial | Jun 25, 2020 | hkumar 

1. Short Intro

std::reference_wrapper<T> is a copyable and assignable object that imitates a reference (T&). It gives the non-nullable guarantee of a reference and the pointer-like flexibility to rebind to another object.

The usual way to create an std::reference_wrapper<T> is via std::ref (or std::cref for reference_wrapper<const T>). A contrived example:

template<typename N>
void change(N n) {
 //if n is std::reference_wrapper<int>, 
 // it implicitly converts to int& here.
 n += 1; 
}

void foo() {
 int x = 10; 

 int& xref = x;
 change(xref); //Passed by value 
 //x is still 10
 std::cout << x << "\n"; //10

 //Be explicit to pass by reference
 change<int&>(x);
 //x is 11 now
 std::cout << x << "\n"; //11

 //Or use std::ref
 change(std::ref(x)); //Passed by reference
 //x is 12 now
 std::cout << x << "\n"; //12
}

Read on for details.

2. Motivation

C++ references are favored over pointers wherever possible because they cannot be null, appropriately express by-reference intent, and have better readability because of no dereferencing (*, ->) jumble.

However, a reference is a stubborn alias of an object. That presents some challenges, like the one shown above where the function template parameter type has to be explicitly specified to pass by reference.

Also, we cannot get the address of a reference itself because a reference is not an object (a region of storage) according to the C++ standard. Declaring a reference to a reference, an array of references, and a pointer to a reference is forbidden in C++. Because of that, reference types do not meet the Erasable requirement of STL container elements. Therefore, we cannot have a container (e.g., vector or list) of reference elements:

std::vector<int&> v; //Error

Moreover, a reference cannot rebind to another object. Therefore, assigning a reference to another does not assign the reference itself; it assigns the object:

int x=10, y=20;
int &xref = x, &yref = y;

xref = yref;
//xref still refers x, which is 20 now.

As references cannot rebind, having a reference class member is a pain in the neck because a reference member makes a class non-assignable — the default copy-assignment operator is deleted. Move-semantics does not make sense with a reference member altogether.

An std::reference_wrapper is a copyable and assignable object that emulates a reference. Contrary to its name, it does not wrap a reference. It works by encapsulating a pointer (T*) and by implicitly converting to a reference (T&). It cannot be default constructed or initialized with a temporary; therefore, it cannot be null or invalid:

std::reference_wrapper<int> nr; //Error! must initialize.

std::string str1{"Hello"};
std::string str2{"World"};
auto r1 = std::ref(str1); //OK
auto r2 = std::ref(str2); //OK
//Assignment rebinds the reference_wrapper  
r2 = r1;  //r2 also refers to str1 now 
//Implicit conversion to std::string&
std::string cstr = r2; //cstr is "Hello"

//Possible to create an array of reference_wrapper
std::reference_wrapper<std::string> arr[] = {str1, str2}; 

auto r2 = std::ref(std::string("Hello")); //Error! no temporary (rvalue) allowed.

reference_wrapper


The only downside is that to access the members of an object (T), we have to use the 'std::reference_wrapper<T>::get' method:

std::string str{"Hello"};

auto sref = std::ref(str); 
//Print length of str
std::cout << sref.get().length() << "\n"; //5

Also, to assign to the referred object use 'get()':

sref.get() = "World"; //str is changed to "World"
std::cout << str << "\n"; //World

Hopefully, using 'get()' would not be necessary when C++ supports overloading operator dot(.) and the concept of smart-references someday. See proposal N4477.

We can have a vector of std::reference_wrapper, have it as a member of a class, and more as we see in the next section.

3. Common Use Cases

3.1. With make_pair and make_tuple

std::reference_wrapper can be used as an argument to a template function (or constructor) to avoid specifying the template parameter types explicitly. A peculiar case here is of make_pair (make_tuple) whose purpose is to cut the verbosity associated with instantiating a pair (tuple). Compare these:

int m=10, n=20;
std::string s{"Hello"};

std::pair<int&, int> p1(m, n);
std::tuple<int&, int, std::string&> t1(m, n, s);
//Vs.
auto p2 = std::make_pair(std::ref(m), n);
auto t2 = std::make_tuple(std::ref(m), n, std::ref(s));

Another advantage of reference_wrapper is that it cannot be instantiated with a temporary. E.g., the following is an undefined-behavior (UB) because the temporary's life is extended only until the constructor parameter goes out of scope:

std::string yell() { return "hey"; }

std::pair<const std::string&, int> p3(yell(), n); //Bad!
//The temporary std::string("hey") is already destroyed here.
//Accessing p3.first is UB here. 

The above case is of dangling reference, which can be avoided with reference_wrapper:

//However, safe with std::ref
auto p4 = std::make_pair(std::ref(yell()), n); //Error!, good 

The make_pair and make_tuple are somewhat irrelevant since the C++17's class template argument deduction (CTAD). But the reasons still apply, now, with the constructors:

std::pair p5(std::ref(m), n); 

std::tuple t3(std::ref(m), n, std::ref(s));

However, there is a subtlety associated with make_pair and make_tuple, which should not be so important in most cases. The make_pair and make_tuple decay a reference_wrapper<T> to a reference (T&), whereas, that is not the case with CTAD construction of pair and tuple.

3.2. Container of references

Unlike a reference, a reference_wrapper is an object and thus satisfies the STL container element requirements (Erasable, to be precise). Therefore, reference_wrapper can be used as a vector element type.

A reference_wrapper<T> could be a safe alternative to a pointer type (T*) for storing in a vector:

using namespace std;
vector<reference_wrapper<int>> v;  //OK
int a=10;
v.push_back(std::ref(a));

3.3. Passing args by reference to start-function via std::thread

We can pass arguments to the start-function when creating a new thread via std::thread(startFunction, args). Those arguments are passed by value from the thread creator function because the std::thread constructor copies or moves the creator's arguments before passing them to the start-function.

So, a start-function's reference parameter cannot bind to a creator's argument. It can only bind to a temporary created by std::thread:

void start(int& i) { i += 1; }
void start_const(const int& i) { }

void create() {
 int e = 10; 
 //e is copied below to a temporary
 std::thread(start, e).join(); //Error! can't bind temporary to int&.
 std::thread(start_const, e).join(); //OK. But sort of by-value 
}

If we want to pass an argument to a start-function by reference, we do that through std::ref, as follows:

void create() {
 int e = 10; 
 std::thread(start, std::ref(e)).join(); //OK. By-ref
 //e is 11 now

 std::thread(start_const, std::ref(e)).join(); //By-ref 
 std::thread(start_const, std::cref(e)).join(); //By-ref 
}

Above, std::ref generates a reference_wrapper<int> that eventually implicitly converts to an int&, thus binding the start(int&)'s reference parameter to the argument passed by the create().

3.4. Reference as a class member

Having a reference class member poses problems, such that, it makes the class non-assignable and practically immovable:

struct W {
 W(int& i):iRef(i) {}
 int& iRef;
};

int u=10, v=20;
W w1(u); 
W w2(v);
w1 = w2; //Error! implicitly deleted copy-assignment operator

The usual practice is to avoid references as class members and use pointers instead.

A reference_wrapper offers the best of both worlds:

struct W {
 W(int& i):iRef(i) {}
 std::reference_wrapper<int> iRef;
};

W w3(u); 
W w4(v);
w3 = w4; //OK

3.5. Passing a function object by reference

An std::reference_wrapper<T> can be invoked like a function as long as the T is a callable. This feature is particularly useful with STL algorithms if we want to avoid copying a large or stateful function object.

Besides, T can be any callable – a regular function, a lambda, or a function object. For example:

struct Large {
 bool operator()(int i) const {
  //Filter and process
  return true;
 }
 //big data
};

const Large large; //Large immutable data and function object

std::vector<int> in1; //input vector
std::vector<int> in2; //input vector

void process() {
 std::vector<int> out;
 //Pass Large by-ref to avoid copy 
 std::copy_if(in1.begin(), in1.end(), std::back_inserter(out), std::ref(large));  
 std::copy_if(in2.begin(), in2.end(), std::back_inserter(out), std::ref(large)); 
 //use the filtered 'out' vector
}

3.6. With bind expressions

std::bind generates a callable wrapper known as bind expression that forwards the call to a wrapped-callable. A bind expression can have some or all of the wrapped-callable's parameters bound.

However, the bound arguments are copied or moved in a bind expression.

void caw(const std::string& quality, const std::string& food) {
 std::cout << "A " << quality << " " << food << "\n";
}

using namespace std::placeholders;  // for _1, _2

std::string donut("donut");
auto donutcaw = std::bind(caw, _1, donut); //donut is copied
donutcaw("chocolate"); //A chocolate donut

Therefore, we need to use std::ref (or std::cref) if we want to pass the bound parameters by reference:

std::string muffin("muffin");
auto muffincaw = std::bind(caw, _1, std::ref(muffin)); //muffin is passed by-ref
muffincaw("delicious"); //A delicious muffin

4. Further Readings

std::reference_wrapper - cppreference

reference_wrapper for incomplete types - Proposal