Arrays of references via std::reference_wrapper

In the prior lesson, we mentioned that arrays can have elements of any object type. This includes objects with fundamental types (e.g. int) and objects with compound types (e.g. pointer to int).

#include <array>
#include <iostream>
#include <vector>

int main()
{
    int a { 10 };
    int b { 20 };

    [[maybe_unused]] std::array valData { a, b };   // an array of int values
    [[maybe_unused]] std::vector ptrData { &a, &b }; // a vector of int pointers

    return 0;
}

However, because references are not objects, you cannot make an array of references. The elements of an array must also be assignable, and references can't be reseated.

#include <array>
#include <iostream>

int main()
{
    int a { 10 };
    int b { 20 };

    [[maybe_unused]] std::array<int&, 2> refData { a, b }; // compile error: cannot define array of references

    int& ref1 { a };
    int& ref2 { b };
    [[maybe_unused]] std::array valData { ref1, ref2 }; // ok: this is actually a std::array<int, 2>, not an array of references

    return 0;
}

In this lesson, we'll use std::array in the examples, but this is equally applicable to all array types.

However, if you want an array of references, there is a workaround.

std::reference_wrapper

std::reference_wrapper is a standard library class template that lives in the header. It takes a type template argument T, and then behaves like a modifiable lvalue reference to T.

There are a few things worth noting about std::reference_wrapper:

  • Operator= will reseat a std::reference_wrapper (change which object is being referenced).
  • std::reference_wrapper<T> will implicitly convert to T&.
  • The get() member function can be used to get a T&. This is useful when we want to update the value of the object being referenced.

Here's a simple example:

#include <array>
#include <functional> // for std::reference_wrapper
#include <iostream>

int main()
{
    int a { 10 };
    int b { 20 };
    int c { 30 };

    std::array<std::reference_wrapper<int>, 3> data { a, b, c };

    data[1].get() = 50; // modify the object in array element 1

    std::cout << data[1] << b << '\n'; // show that we modified data[1] and b, prints 5050

    return 0;
}

This example prints:

5050

Note that we must use data[1].get() = 50 and not data[1] = 50. The latter is ambiguous, as the compiler can't tell if we intend to reseat the std::reference_wrapper<int> to value 50 (something that is illegal anyway) or change the value being referenced. Using get() disambiguates this.

When printing data[1], the compiler will realize it can't print a std::reference_wrapper<int>, so it will implicitly convert it to an int&, which it can print. So we don't need to use get() here.

std::ref and std::cref

Prior to C++17, CTAD (class template argument deduction) didn't exist, so all template arguments for a class type needed to be listed explicitly. Thus, to create a std::reference_wrapper<int>, you could do either of these:

    int a { 5 };

    std::reference_wrapper<int> ref1 { a };        // C++11
    auto ref2 { std::reference_wrapper<int>{ a }}; // C++11

Between the long name and having to explicitly list the template arguments, creating many such reference wrappers could be tedious.

To make things easier, the std::ref() and std::cref() functions were provided as shortcuts to create std::reference_wrapper and const std::reference_wrapper wrapped objects. Note that these functions can be used with auto to avoid having to explicitly specify the template argument.

    int a { 5 };
    auto ref { std::ref(a) };   // C++11, deduces to std::reference_wrapper<int>
    auto cref { std::cref(a) }; // C++11, deduces to std::reference_wrapper<const int>

Of course, now that we have CTAD in C++17, we can also do this:

    std::reference_wrapper ref1 { a };        // C++17
    auto ref2 { std::reference_wrapper{ a }}; // C++17

But since std::ref() and std::cref() are shorter to type, they are still widely used to create std::reference_wrapper objects.

Summary

Arrays cannot hold references: Because references are not objects and cannot be reseated, you cannot create arrays of references directly. Array elements must be assignable objects.

std::reference_wrapper as a workaround: This standard library class template behaves like a modifiable lvalue reference but is an object, making it compatible with arrays. It lives in the <functional> header.

Key behaviors: operator= reseats the wrapper (changes which object is referenced), implicit conversion to T& allows transparent use, and get() explicitly retrieves the reference (required when modifying the referenced value to avoid ambiguity).

std::ref() and std::cref(): These helper functions create std::reference_wrapper (and const versions) more concisely than explicit template instantiation. They work with auto type deduction and remain popular even after C++17 CTAD.

Disambiguation with get(): When modifying the referenced value, use data[1].get() = 50 instead of data[1] = 50. The latter is ambiguous (reseating vs modifying), while get() makes the intent clear.

Implicit conversion for reading: When reading values or passing to functions, the compiler implicitly converts std::reference_wrapper<T> to T&, so get() isn't needed.

This technique allows creating collections of references, useful when you need arrays that reference existing objects rather than storing copies, while maintaining the ability to reseat references after initialization.