1. Introduction
References in C++ are aliases to objects. They are not guaranteed to occupy storage, cannot be reassigned, and are not objects themselves. Pointers, on the other hand, point to objects while being reassignable objects. While references and pointers serve much the same purpose, references provide an important guarantee that they will never refer to a null object. Thus, there is a gap in core language and library support for representing non-nullable but reassignable indirection:
Not reassignable | Reassignable | |
---|---|---|
Nullable |
|
|
Non-nullable |
| - |
When faced with a problem where such a non-nullable reassignable reference type would be useful, developers must either
switch to pointers or use another wrapper type with these properties. Due to its availability,
has emerged as an existing practice for this use case. However, because it was designed for certain metaprogramming uses
requiring references that behave like regular objects as opposed to serving as a more general utility type, it is not
very ergonomic. The two most notable shortcomings are that the name is long and cumbersome and the only ways to access
the referenced object are a conversion operator or
instead of a forwarding member access operator.
Ergonomic limitations of
have led some developers to create their own reference wrapper-like types
that are more ergonomic in lieu of
’s shortcomings. For example, Barry Revzin shared on the
mailing list in September 2024:
In reply to a question asking if
was not already sufficient:
. get () For what it’s worth, we have a template that inherits from
specifically for this reason. After a lot of use,
reference_wrapper < T > is pretty verbose compared to
r . get (). f () .
r -> f () Well that and the name
is also very verbose so ours is just
reference_wrapper < T > .
Ref < T >
Both the name and constantly having to call
results in this utility being quite cumbersome to use.
would be similarly cumbersome if developers had to constantly call
as opposed to being able
to use
for member access.
This paper proposes a small set of ergonomic improvements to address these limitations and transform the type from a metaprogramming utility to a more broadly applicable vocabulary type that closes the gap between nullability and reassignability.
2. Motivation
References are often preferred in C++ because they do not require careful thought about nullability. However, there are
times when references can’t be used due to not behaving like objects and not being reassignable. For example, it’s
generally not possible to implement a copy or move assignment operator for a class with non-static reference members and
implicitly-declared copy and move assignment operators are defined as deleted. Additionally, due to not behaving like
normal objects, all of the following are ill-formed:
,
,
,
, and
. Notably,
is also ill-formed,
however, [P2988] is on track for acceptance. Whenever any of the aforementioned constructs are desired, or a
programmer would simply like to reassign a
, the limitations of references result in either needing to switch to
a pointer or another wrapper type.
Pointers work as replacements for references when object-like behavior or reassignability is desirable. However, they do not reflect non-nullability in the type system. Losing this valuable information in the type system has implications with bug-proneness and may also lead to unnecessary null checks out of an abundance of caution in code paths where non-nullability cannot be readily shown. This can increase complexity and clutter code.
The desire for a non-nullable, reassignable, well-behaved reference has led many to reach for
as a
standard utility with these semantics. For example, a highly upvoted comment on Why does std::optional not have a specialization for reference types? mentions using
as a workaround.
While
was initially added for certain metaprogramming uses, it has become a fairly widespread
pattern that can be seen in codebases ranging from hobby projects to large and established codebases such as Chromium. A
cursory GitHub code search for
in open-source C++ code at the time of writing yields 45k results: Code Search.
Many of these uses are:
-
as a non-static data memberstd :: reference_wrapper < T > -
std :: vector < std :: reference_wrapper < T >> -
std :: optional < std :: reference_wrapper < T >> -
std :: map < K , std :: reference_wrapper < V >> -
Use in
std :: variant -
Use in other containers
The first few pages of results suggest these categories account for the vast majority of use of
in
open-source C++ code.
Due to its semantics and status as an existing practice for this use case, it’s fitting to expand the intended use case
of
and improve its ergonomics as a general utility instead of introducing a new vocabulary type for
this use case.
3. Alternatives
Alternatives to
for this use case include:
-
Tell developers to just use pointers
-
Add a
pointer wrapper to fill the gap insteadstd :: not_null Not reassignable Reassignable Nullable T * const T * Non-nullable T &
,reference_wrapper not_null -
Add first-order language support for reassignable references or references that behave as normal objects
Pointers are undesirable for reasons mentioned earlier.
There is prior art for
in the form of
. This wrapper type fundamentally mimics a pointer,
though, and has constructors taking pointers that are null-checked at runtime.
, on the other hand,
is designed to model a reference: It’s constructed only by a reference, and thus, non-nullability is ensured as an
invariant at the type system level.
First-order language support for references that behave like objects and are assignable is a possibility, but, it would
be a massive change to the language. Instead, improving
for this use case seems preferable.
4. Proposal
This paper proposes three changes:
-
Add
as an aliastemplate < typename T > using reference = reference_wrapper < T > ; -
Add an
overload for forwarding member access as well as anoperator ->
overload for symmetryoperator * -
Provide
and thereference_wrapper
alias through thereference
header< utility >
is chosen as a shorter alias for
due to
already being used.
Expanding the forwarding operations for
follows the precedent of [P2944], which added forwarding
comparison operators to
. While
is typically associated with pointers and pointer-like
types (such as iterators or smart pointers), it is the only option for member access forwarding in lieu of
.
This use of
isn’t without precedent, and it isn’t uncommon even for types that aren’t pointer-like. For
example, all of the following have
overloads for convenience:
,
, or types
like
/
(stackoverflow) or
(boost json, [P0308]).
is arguably similar to a pointer in that it is nullable; however,
and the proxy/wrapper
types are less so. Fundamentally, the main commonality between all these types isn’t nullability but rather serving as
proxies to other objects via either wrapping or indirection. As such,
is fitting for
.
With the expanded use case for
, it is better provided through
so that
does not have to be pulled in just to use the type. This follows the precedent of [P0472], which added
to
. This will require that implementations either
from
or put
in a common implementation header that can be included in both places.
5. Proposed Wording
Proposed wording relative to [N4950]:
Move [refwrap] and subsections to [utility.refwrap].
Move from [functional.syn] to [utility.syn]:
// [ utility. refwrap], reference_wrapper template < class T > class reference_wrapper ; // freestanding template < class T > constexpr reference_wrapper < T > ref ( T & ) noexcept ; // freestanding template < class T > constexpr reference_wrapper < const T > cref ( const T & ) noexcept ; // freestanding template < class T > void ref ( const T && ) = delete ; // freestanding template < class T > void cref ( const T && ) = delete ; // freestanding template < class T > constexpr reference_wrapper < T > ref ( reference_wrapper < T > ) noexcept ; // freestanding template < class T > constexpr reference_wrapper < const T > cref ( reference_wrapper < T > ) noexcept ; // freestanding // [ utility. refwrap.common.ref], common_reference related specializations template < class R , class T , template < class > class RQual , template < class > class TQual > requires see below struct basic_common_reference < R , T , RQual , TQual > ; template < class T , class R , template < class > class TQual , template < class > class RQual > requires see below struct basic_common_reference < T , R , TQual , RQual > ;
Add to [utility.syn]:
// [utility.refwrap], reference_wrapper template < class T > class reference_wrapper ; // freestanding template < class T > constexpr reference_wrapper < T > ref ( T & ) noexcept ; // freestanding template < class T > constexpr reference_wrapper < const T > cref ( const T & ) noexcept ; // freestanding template < class T > void ref ( const T && ) = delete ; // freestanding template < class T > void cref ( const T && ) = delete ; // freestanding template < class T > constexpr reference_wrapper < T > ref ( reference_wrapper < T > ) noexcept ; // freestanding template < class T > constexpr reference_wrapper < const T > cref ( reference_wrapper < T > ) noexcept ; // freestanding template < class T > using reference = reference_wrapper < T > ; // freestanding // [utility.refwrap.common.ref], common_reference related specializations template < class R , class T , template < class > class RQual , template < class > class TQual > requires see below struct basic_common_reference < R , T , RQual , TQual > ; template < class T , class R , template < class > class TQual , template < class > class RQual > requires see below struct basic_common_reference < T , R , TQual , RQual > ;
Add a paragraph at the end of [functional.syn]:
The class templates, function templates, and alias template defined in [utility.refwrap] are available when < functional >
is included.
Add to [utility.refwrap.general]:
namespace std { template < class T > class reference_wrapper { public : // types using type = T ; // [utility.refwrap.const], constructors template < class U > constexpr reference_wrapper ( U && ) noexcept ( see below ); constexpr reference_wrapper ( const reference_wrapper & x ) noexcept ; // [utility.refwrap.assign], assignment constexpr reference_wrapper & operator = ( const reference_wrapper & x ) noexcept ; // [utility.refwrap.access], access constexpr operator T & () const noexcept ; constexpr T & get () const noexcept ; constexpr T * operator -> () const noexcept ; constexpr T & operator * () const noexcept ; // [utility.refwrap.invoke], invocation template < class ... ArgTypes > constexpr invoke_result_t < T & , ArgTypes ... > operator ()( ArgTypes && ...) const noexcept ( is_nothrow_invocable_v < T & , ArgTypes ... > ); // [utility.refwrap.comparisons], comparisons friend constexpr bool operator == ( reference_wrapper , reference_wrapper ); friend constexpr bool operator == ( reference_wrapper , const T & ); friend constexpr bool operator == ( reference_wrapper , reference_wrapper < const T > ); friend constexpr auto operator <=> ( reference_wrapper , reference_wrapper ); friend constexpr auto operator <=> ( reference_wrapper , const T & ); friend constexpr auto operator <=> ( reference_wrapper , reference_wrapper < const T > ); }; template < class T > reference_wrapper ( T & ) -> reference_wrapper < T > ; }
Add to [utility.refwrap.access]:
Returns:
.
Returns: The stored reference.