What Are Arrays of References via std::reference_wrapper?

An array element has to be two things: an object, and assignable. A reference is neither. It is not an object, and once bound it cannot be made to refer to something else. That is why no array flavour, C-style or std::array or std::vector, will hold references.

std::reference_wrapper<T> is the standard library's way around that. It lives in the <functional> header, it stores the address of a T, and it spends the rest of its life impersonating a T&. Because it is an ordinary copyable, assignable class object, an array is perfectly happy to hold it.

The examples below run a small water supply. Each reservoir holds some number of megalitres, tracked in its own int, and the treatment works keeps a duty rota naming which reservoirs it is drawing from. The rota must not copy the levels, because drawing water has to change the real figures, and any slot in the rota may be pointed at a different reservoir later in the season. That is exactly the job std::reference_wrapper exists for.

Where the Compiler Stops You

Here is the direct attempt. This program does not compile, and it is included to show the diagnostic:

int main()
{
    int hillTarn{ 184 };
    int valleyBasin{ 236 };

    int& intakes[2]{ hillTarn, valleyBasin };

    return 0;
}

The compiler names the problem outright:

s.cpp: In function 'int main()':
s.cpp:6:10: error: declaration of 'intakes' as array of references
    6 |     int& intakes[2]{ hillTarn, valleyBasin };
      |          ^~~~~~~

Writing the same idea as std::array<int&, 2> fails too, only the diagnostic arrives from deep inside the header instead. std::array needs a value_type* for its iterators, and there is no such thing as a pointer to a reference, so the instantiation collapses.

The Copy That Looks Like a Reference

There is a quieter failure worth knowing, because this one compiles:

#include <array>
#include <iostream>

int main()
{
    int hillTarn{ 184 };
    int valleyBasin{ 236 };

    int& firstIntake{ hillTarn };
    int& secondIntake{ valleyBasin };

    std::array intakes{ firstIntake, secondIntake }; // deduced as std::array<int, 2>

    intakes[0] -= 30;

    std::cout << "intakes[0] is " << intakes[0] << '\n';
    std::cout << "hillTarn is " << hillTarn << '\n';

    return 0;
}
intakes[0] is 154
hillTarn is 184

Feeding references into class template argument deduction does not produce an array of references. Deduction strips the reference and lands on int, so intakes is a std::array<int, 2> holding two copies. Drawing 30 megalitres from the array leaves the reservoir untouched, which is the opposite of what the rota is for.

Three Ways to Aim a Collection at Existing Objects

With references ruled out, three element types are left in play, and they differ mainly in what each one costs you at the point of use:

Element type What the element holds Read it Write through it Aim it elsewhere
int a copy of the value rota[0] changes only the copy not applicable
int* an address, possibly null *rota[0] *rota[0] -= 40 rota[0] = &hillTarn
std::reference_wrapper<int> an address, never null rota[0] rota[0].get() -= 40 rota[0] = hillTarn
int& rejected, no array of references not available not available not available

Pointers would work, at the price of a * on every read and a null state you have to keep checking for. std::reference_wrapper has no null state, since it can only be built from an existing lvalue, and it reads without any punctuation at all. The tradeoff is that writing costs a get(), for reasons the next two sections cover.

Building the Rota

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

int main()
{
    int hillTarn{ 184 };
    int valleyBasin{ 236 };
    int coastalStore{ 97 };

    // Three duty slots, each referring to one of the reservoirs above.
    std::array<std::reference_wrapper<int>, 3> dutyRota{ hillTarn, valleyBasin, coastalStore };

    dutyRota[2].get() -= 40; // draw 40 megalitres through slot 2

    std::cout << "slot 2 reads " << dutyRota[2] << '\n'; // implicit conversion, no get() needed
    std::cout << "coastalStore is " << coastalStore << '\n';

    return 0;
}
slot 2 reads 57
coastalStore is 57

Both lines print the same number because there is only one number. dutyRota[2] does not store a level, it stores the address of coastalStore, and the subtraction went straight to the reservoir.

Notice that the printing line needs no get(). Streaming a std::reference_wrapper<int> finds no overload that takes one, so the implicit conversion to int& kicks in and operator<< prints that instead. The same conversion applies anywhere a T& is expected: passing the slot to a function taking int& or int works with no ceremony.

Key Concept
A std::reference_wrapper<T> converts to T& implicitly, so reading looks exactly like reading the original object. get() is only needed where the conversion cannot be applied, and assignment is the main such place.

Why the Write Needs get()

Dropping the get() looks reasonable and is not. This program does not compile:

#include <array>
#include <functional>

int main()
{
    int hillTarn{ 184 };
    int valleyBasin{ 236 };
    int coastalStore{ 97 };

    std::array<std::reference_wrapper<int>, 3> dutyRota{ hillTarn, valleyBasin, coastalStore };

    dutyRota[2] = 57; // meant to write 57 into coastalStore

    return 0;
}
s.cpp: In function 'int main()':
s.cpp:12:19: error: no match for 'operator=' (operand types are 'std::array<std::reference_wrapper<int>, 3>::value_type' {aka 'std::reference_wrapper<int>'} and 'int')
   12 |     dutyRota[2] = 57; // meant to write 57 into coastalStore
      |                   ^~

There is exactly one assignment operator on a wrapper, and it takes another wrapper. Assigning to the slot therefore means "refer to whatever that other wrapper refers to", never "store this value in the reservoir". An int literal cannot become a wrapper, because a wrapper has to bind to something with an address, so the one candidate is rejected and compilation stops.

Those two readings, reseat the slot versus modify the reservoir, are precisely what get() separates. get() hands back the int&, and assigning to an int& has only one possible meaning.

Best Practice
Read through the wrapper directly and write through get(). Treat every bare = on a wrapper as a reseat, because that is what the language treats it as.

Assignment Reseats the Slot

That reseating behaviour is a feature, and it is the one thing a real reference cannot do:

#include <array>
#include <functional>
#include <iostream>

int main()
{
    int hillTarn{ 184 };
    int valleyBasin{ 236 };
    int coastalStore{ 97 };

    std::array<std::reference_wrapper<int>, 3> dutyRota{ hillTarn, valleyBasin, coastalStore };

    dutyRota[2] = hillTarn;  // reseat: slot 2 now refers to hillTarn
    dutyRota[2].get() -= 25; // so this draws from hillTarn

    std::cout << "hillTarn is " << hillTarn << '\n';
    std::cout << "coastalStore is " << coastalStore << '\n';
    std::cout << "slot 0 and slot 2 read " << dutyRota[0] << " and " << dutyRota[2] << '\n';

    return 0;
}
hillTarn is 159
coastalStore is 97
slot 0 and slot 2 read 159 and 159

coastalStore kept its 97 because slot 2 stopped referring to it before the subtraction happened. Slots 0 and 2 now both name hillTarn, and both report the drawdown. Note what dutyRota[2] = hillTarn; did: it built a temporary wrapper around hillTarn and copied that into the slot, changing the target rather than the value. Reseating is what made an array possible in the first place, since array elements must be assignable and true references are not.

Warning
slot = otherReservoir; and slot.get() = otherReservoir; both compile and do completely different things. The first repoints the slot; the second overwrites the level of whatever the slot currently refers to.

const Does Not Travel Through the Wrapper

Marking the array const protects the array, not the reservoirs behind it:

#include <array>
#include <functional>
#include <iostream>

using DutyRota = std::array<std::reference_wrapper<int>, 3>;

void drawEvenly(const DutyRota& rota, int megalitres)
{
    for (auto slot : rota)
        slot.get() -= megalitres; // const on the array does not reach the reservoirs
}

int main()
{
    int hillTarn{ 184 };
    int valleyBasin{ 236 };
    int coastalStore{ 97 };

    DutyRota dutyRota{ hillTarn, valleyBasin, coastalStore };

    drawEvenly(dutyRota, 12);

    std::cout << hillTarn << ' ' << valleyBasin << ' ' << coastalStore << '\n';

    return 0;
}
172 224 85

Every level dropped, through a const parameter. This is the same rule that governs int* const: the constness applies to the handle, and get() on a const std::reference_wrapper<int> still returns a plain int&.

Read-only access has to be spelled in the element type instead, as std::reference_wrapper<const int>. Then the write is rejected where it is written. This program does not compile:

#include <functional>

int main()
{
    int hillTarn{ 184 };

    auto sealedFeed{ std::cref(hillTarn) };

    sealedFeed.get() -= 20;

    return 0;
}
s.cpp: In function 'int main()':
s.cpp:9:22: error: assignment of read-only location 'sealedFeed.std::reference_wrapper<const int>::get()'
    9 |     sealedFeed.get() -= 20;
      |     ~~~~~~~~~~~~~~~~~^~~~~

Shortcuts: std::ref() and std::cref()

std::reference_wrapper predates class template argument deduction. Before C++17 every template argument had to be written out, so building one meant std::reference_wrapper<int> feed{ hillTarn }; and building a const one meant repeating the element type by hand. The library shipped std::ref() and std::cref() as function templates that deduce the argument for you, and they are still the shortest way to say it:

#include <functional> // for std::ref and std::cref
#include <iostream>
#include <utility>    // for std::as_const

int main()
{
    int hillTarn{ 184 };

    auto feed{ std::ref(hillTarn) };                           // std::reference_wrapper<int>
    auto sealedFeed{ std::cref(hillTarn) };                    // std::reference_wrapper<const int>
    std::reference_wrapper<const int> auditFeed{ hillTarn };   // spelled out in full
    std::reference_wrapper deducedFeed{ std::as_const(hillTarn) }; // CTAD deduces const int

    feed.get() += 16; // the non-const wrapper can still write

    std::cout << "hillTarn is " << hillTarn << '\n';
    std::cout << sealedFeed << ' ' << auditFeed << ' ' << deducedFeed << '\n';

    return 0;
}
hillTarn is 200
200 200 200

The last three lines of declarations are three spellings of the same type, std::reference_wrapper<const int>: name it explicitly, let std::cref() deduce it, or let CTAD deduce it from a const int& produced by std::as_const(). All four wrappers refer to the one hillTarn, which is why the write through feed shows up in every read.

C++17 CTAD means std::reference_wrapper feed{ hillTarn }; now works without the angle brackets, but std::ref() stays popular for being shorter, and for reading clearly inside a larger expression.

Danger
A wrapper stores an address and owns nothing. If the object it refers to is destroyed first, the wrapper is left dangling and every use of it is undefined behavior. Returning a wrapper to a local variable, or keeping an array of wrappers alive longer than the objects it names, has all the lifetime hazards of a raw pointer with none of the visual warning signs.

Summary

Arrays cannot hold references: array elements must be objects and must be assignable, and a reference is neither an object nor reseatable. int& intakes[2] is rejected as "declaration of 'intakes' as array of references", and std::array<int&, 2> fails inside the header for the same underlying reason.

Deduction will not rescue you: initializing std::array from reference variables deduces the element type as int, giving you an array of copies that silently disconnects from the originals.

std::reference_wrapper<T> is the workaround: declared in <functional>, it stores the address of a T, has no null state, and is a copyable assignable object, so arrays and other containers accept it.

Reading is implicit, writing uses get(): the wrapper converts to T& wherever a T& is wanted, so printing or passing needs nothing extra. get() returns the T& explicitly and is required when assigning.

Plain = reseats: the only assignment operator takes another wrapper, so slot = 57 does not compile and slot = hillTarn repoints the slot instead of writing to it. get() is what distinguishes reseating from modifying.

const applies to the wrapper, not the referent: a const array of std::reference_wrapper<int> still permits writes through get(). Use std::reference_wrapper<const int> when the collection must be read-only.

std::ref() and std::cref(): shorthand for building non-const and const wrappers with the type deduced. Since C++17 CTAD can deduce it too, including std::reference_wrapper{ std::as_const(x) } for the const version, but the short function names remain in wide use.

Lifetime is your responsibility: the wrapper does not extend the life of anything it refers to.