common_reference_t
of reference_wrapper
Should Be a
Reference TypeDocument #: | P2655R3 |
Date: | 2023-02-07 |
Project: | Programming Language C++ |
Audience: |
SG9, LEWG |
Reply-to: |
Hui Xie <hui.xie1990@gmail.com> S. Levent Yilmaz <levent.yilmaz@gmail.com> Tim Song <t.canens.cpp@gmail.com> |
common_reference
of any
cv-qualified proxy types.T&
const
and
volatile
This paper proposes a fix that makes the common_reference_t<T&, reference_wrapper<T>>
a reference type T&
.
C++20 introduced the meta-programming utility
common_reference
21.3.8.7
[meta.trans.other]
in order to programmatically determine a common reference type to which
one or more types can be converted or bound.
The precise rules are rather convoluted, but roughly speaking, for
given two non-reference non-cv qualified types
X
and
Y
, common_reference<X&, Y&>
is equivalent to the expression
decltype(false ? x : y)
where
x
and
y
are qualified
X&
and
Y&
, respectively, provided
the ternary expression is valid. (cv-qualified references are
treated differently, and is explained below in Section 4.4.) Otherwise,
basic_common_reference
trait is
consulted, which is a customization point that allows users to influence
the result of common_reference
for user-defined types. (Two such specializations are provided by the
standard library, namely, for
std::pair
and
std::tuple
which map
common_reference
to their
respective elements.) And if no such specialization exists, then the
result is
common_type<X,Y>
.
The canonical use of
reference_wrapper<T>
is
its being a surrogate for
T&
. One might expect that
the ternary operator would yield a
T&
, but due to language
rules, that is not quite the case:
int i = 1, j = 2;
::reference_wrapper<int> jr = j; // ok - implicit constructor
stdint & ir = std::ref(i); // ok - implicit conversion
int & r = false ? i : std::ref(j); // error - conditional expression is ambiguous.
The reason for the error is not because
i
and
ref(j)
, an
int&
and a
reference_wrapper<int>
,
are incompatible. It is because they are too compatible! Both types can
be converted to one another, so the type of the ternary expression is
ambiguous.
Hence, per the current rules of
common_reference
as summarized
above, and with the lack of any
basic_common_reference
specialization, the evaluation falls back to common_type<T, reference_wrapper<T>>
,
whose ::type
is valid and equal
to T
. In other words,
common_reference
determines that
the reference type to which both
T&
and a
reference_wrapper<T>
can
bind is a prvalue T
!
The authors believe this current determination logic for
common_reference
for an lvalue
reference to a type T
and its
reference_wrapper<T>
is
merely an accident, and is incompatible with the canonical purpose of
the reference_wrapper
. The
answer should have been T&
.
(Note that, there is no ambiguity with a
reference_wrapper<T>
and
rvalue of T
, since former is
convertible to latter, but not vice versa.)
This article proposes an update to the standard which would change
the behavior of common_reference
to evaluate as T&
given
T&
and an a
reference_wrapper<T>
,
commutatively. Any evolution to implicit conversion semantics of
reference_wrapper
, or of the
ternary operator for that matter, is out of the question. Therefore, the
authors propose to implement this change via providing a partial
specialization of
basic_common_reference
trait.
Below are some motivating examples:
C++20
|
Proposed
|
---|---|
|
|
|
|
|
|
In the second and the third example, the user would like to use
views::join_with
and
views::concat
[P2542R2], respectively, with a range of
Foo
s and a single
Foo
for which they use a
reference_wrapper
to avoid
copies. Both of the range adaptors rely on
common_reference_t
in their
respective implementations (and specifications). As a consequence, the
counter-intuitive behavior manifests as shown, where the resultant
views’ reference type is a prvalue
Foo
. There does not seem to be
any way for the range adaptor implementations to account for such use
cases in isolation.
T&
and not
reference_wrapper<T>
As they can both be converted to each other, the result of
common_reference_t
can be either
of them in theory. However, the authors believe that the users would
expect the result to be T&
.
Given the following example,
auto r = views::concat(foos,
::single(std::ref(foo2_)));
viewsfor (auto&& foo : r) {
= anotherFoo;
foo }
If the result is
reference_wrapper<T>
, the
assignment inside the for loop would simply rebind the
reference_wrapper
to a different
instance. On the other hand, if the result is
T&
, the assignment would
call the copy assignment operator of the original
foo
s. The authors believe that
the latter design is the intent of code and is the natural choice.
The following are some of the alternatives that considered originally. But later dropped in favor of the one discussed in the next section.
One option would be to provide customisations for only
reference_wrapper<T>
and
cv-ref T
. Note that this version
is rather restrictive:
template <class T, class U, template <class> class TQual,
template <class> class UQual>
requires std::same_as<T, remove_cv_t<U>>
struct basic_common_reference<T, reference_wrapper<U>, TQual, UQual> {
using type = common_reference_t<TQual<T>, U&>;
};
template <class T, class U, template <class> class TQual,
template <class> class UQual>
requires std::same_as<remove_cv_t<T>, U>
struct basic_common_reference<reference_wrapper<T>, U, TQual, UQual> {
using type = common_reference_t<T&, UQual<U>>;
};
reference_wrapper<T>
as
T&
This options completely treats
reference_wrapper<T>
as
T&
and delegates common_reference<reference_wrapper<T>, U>
to the common_reference<T&, U>
.
Therefore, it would support any conversions (including derived-base
conversion) that T&
can
do.
template <class T, class U, template <class> class TQual, template <class> class UQual>
requires requires { typename common_reference<TQual<T>, U&>::type; }
struct basic_common_reference<T, reference_wrapper<U>, TQual, UQual> {
using type = common_reference_t<TQual<T>, U&>;
};
template <class T, class U, template <class> class TQual, template <class> class UQual>
requires requires { typename common_reference<T&, UQual<U>>::type; }
struct basic_common_reference<reference_wrapper<T>, U, TQual, UQual> {
using type = common_reference_t<T&, UQual<U>>;
};
Immediately, it run into ambiguous specialisation problems for the following example
<reference_wrapper<int>, reference_wrapper<int>>; common_reference_t
A quick fix is to add another specialisation
template <class T, class U, template <class> class TQual, template <class> class UQual>
requires requires { typename common_reference<T&, U&>::type; }
struct basic_common_reference<reference_wrapper<T>, reference_wrapper<U>, TQual, UQual> {
using type = common_reference_t<T&, U&>;
};
However, this has some recursion problems.
<reference_wrapper<reference_wrapper<int>>,
common_reference_t<int>&>; reference_wrapper
The user would expect the above expression to yield reference_wrapper<int>&>
.
However it yields int&
due
to the recursion logic in the specialisation.
And even worse,
<reference_wrapper<reference_wrapper<int>>,
common_reference_tint&>;
The above expression would also yield
int&
due to the recursion
logic, even though the nested
reference_wrapper
is not
convertible_to<int&>
.
The rational behind this option is that
reference_wrapper<T>
behaves exactly the same as
T&
. But does it?
There is conversion from
reference_wrapper<T>
to
T&
, and if the result
requires another conversion, the language does not allow
reference_wrapper<T>
to be
converted to the result.
This would cover majority of the use cases. However, this does not
cover the derive-base conversions, i.e. common_reference_t<reference_wrapper<Derived>, Base&>>
.
This is a valid use case and the authors believe that it is important to
support it.
The above exposure can be extrapolated to any cv-qualified
or other cross-type compatible conversions. That is, if
common_reference_t<U, V>
exists then common_reference_t<reference_wrapper<U>, V>
and common_reference_t<U, reference_wrapper<V>>
should also exist and be equal to it, given the only additional
requirement that
reference_wrapper<U>
or
reference_wrapper<V>
,
respectively, can be also implicitly converted to
common_reference_t<U,V>
.
This statement only applies when the evaluation of
common_reference_t
falls through
to basic_common_reference
(see
next section).
The authors propose to support such behavior by allowing
basic_common_reference
specialization to delegate the result to that of the
common_reference_t
of the
wrapped type with the other non-wrapper argument. Furthermore, impose
additional constraints on this specialization to make sure that the
reference_wrapper
is convertible
to this result.
In order to support commutativity, we need to introduce two separate specializations, and further constrain them to be mutually exclusive in order avoid ambiguity.
Finally, we have to explicitly disable the edge cases with nested
reference_wrapper
s since, while
reference_wrapper<reference_wrapper<T>>
is not
convertible_to<T&>
reference_wrapper
and Other
Proxy TypesAs implied in the previous sections, the rules of the
common_reference
trait are such
that any basic_common_reference
specialization is consulted only if some ternary expression of the pair
of arguments is ill-formed (see 21.3.8.7
[meta.trans.other]/5.3.1).
More precisely, that ternary expression is denoted by
COMMON-REF(T1, T2)
,
where T1
and
T2
are the two arguments of the
trait, and COMMON-REF
is a complex macro defined in 21.3.8.7
[meta.trans.other]/2.
For the cases where both T1
and
T2
are lvalue references, their
COMMON-REF
is the union
of their cv-qualifiers applied to both. For example, given
T1
is
const X&
and
T2
is
Y&
(where
X
and
Y
are non-reference types), the
evaluated expression is
decltype(false ? xc : yc)
where
xc
and
yc
are
const X&
and
const Y&
, respectively. Note
that, the union of cv-qualifiers is
const
and it is applied to
both arguments even though originally
T2
is a
non-const
reference.
The origin and rationale for these contrived rules are rather
obscure. But one consequence in the context of this paper is that there
are interesting edge cases where the
basic_common_reference
treatment
do not apply. Take,
int i = 3;
const std::reference_wrapper<int> r = i;
int& j = r; // ok.
That is, any cv qualification of
reference_wrapper<int>
itself does not change its semantics, since it is just a proxy to an
int&
. So, it would be
natural to expect that int&
should be the common reference of
int&
and const reference_wrapper<int>&
,
since objects of both types can be assigned to an
int&
.
However, because of the way
COMMON-REF
is defined,
the evaluated ternary expression is
decltype(false ? r : jc)
, where
jc
is
const int&
.
Lo and behold, this expression is no longer ill-formed and evaluates to
const int&
(the conversion
direction is no longer ambiguous, since
reference_wrapper<int>
can
not be constructed from an
int const&
), and we get:
// in the current standard, with or without the basic_common_reference
// specialization of this proposal:
static_assert(std::same_as<
::common_reference_t<
stdconst std::reference_wrapper<int>&,
int&
>,
const int& // not int& !!
>);
This issue exists not only in
reference_wrapper
, but any
proxy-like types with reference cast operators. For example,
struct A {};
struct B {
operator A& () const;
};
Even though the builtin ternary operator
?:
does return the expected type
A&
,
A a;const B b;
static_assert(std::same_as<
decltype(false? a : b),
&
A>);
common_reference_t
surprisingly results in
const A&
static_assert(std::same_as<
::common_reference_t<A&, const B&>,
stdconst A& // not A& !!
>);
Per SG9’s direction, we’d like to fix this issue along with the
basic_common_reference
treatment
in this paper. Let’s revisit the precise rules of
common_reference
trait [meta.trans.other#5.3].
Its member ::type
is,
COMMON-REF
if not
ill-formed.basic_common_reference
if a
specialization exists.decltype
of
ternary operator ?:
.common_type
.The reason why common_reference_t<const reference_wrapper<int>&, int&>
does not use the
basic_common_reference
specialization and why common_reference_t<A&, const B&>
does not use the ternary operator
?:
is that Step-1
COMMON-REF
is well
formed. But it yields an undesired result.
Step-1 is important to have before the customization Step-2, because
the COMMON-REF
layer
provides a generalized mechanism to handle reference cases before the
user-specializable component. This makes the
common_reference
trait more
convenient for users, and more importantly, harder to get wrong. For
example, common_reference<tuple<int>&, tuple<int>&>
should remain
tuple<int>&
and not
tuple<int&>
as would a
straightforward
basic_common_reference
specialization for tuple
s yield
(which is the current one in the standard [tuple#common.ref]). It
would be unreasonable to expect each specialization to handle every
reference combination correctly, exhaustively, and consistently.
Yet, COMMON-REF
was
probably not meant to deal with proxy references and user-defined
conversions at all. It is used only when the types involved are
reference types, and does not make any sense if it were meant to handle
things that are convertible to reference types.
To rectify this situation, the authors propose to reject user-defined conversions entirely from Step-1. This would then allow Step-2 to provide the required custom semantics for any underlying proxy reference types where desired, and Step-3 to recover the ternary-operator based fallback.
This suggestion can be realized by an additional constraint on
Step-1: Require a valid conversion to exist between each respective
pointer types of the pair of arguments to the evaluated
COMMON-REF
result.
Precise implementation can be found in the Wording section.
The authors implemented the proposed wording below without any issue[ours].
The authors also applied the proposed wording in LLVM’s libc++ and all libc++ tests passed.[libcxx]
Modify 21.3.8.7 [meta.trans.other] section (5.3.1) as
R
be
COMMON-REF(T1, T2)
.
If T1
and
T2
are reference types, COMMON-REF(T1, T2)
R
is well-formed, and
is_convertible_v<add_pointer_t<T1>, add_pointer_t<R>> && is_convertible_v<add_pointer_t<T2>, add_pointer_t<R>>
is
true
,
then the member typedef type
denotes R
.Modify 22.10.2
[functional.syn]
to add to the end of
reference_wrapper
section:
common_reference
related
specializations
// [refwrap.common.ref]
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 the following subclause to 22.10.6 [refwrap]:
common_reference
related
specializations [refwrap.common.ref]template <class T>
inline constexpr bool is-ref-wrapper = false; // exposition only
template <class T>
inline constexpr bool is-ref-wrapper<reference_wrapper<T>> = true;
template<class R, class T, class RQ, class TQ>
concept ref-wrap-common-reference-exists-with = // exposition only
<R>
is-ref-wrapper&& requires {
typename common_reference_t<typename R::type&, TQ>;
}
&& convertible_to<RQ, common_reference_t<typename R::type&, TQ>>
;
template <class R, class T, template <class> class RQual, template <class> class TQual>
requires( ref-wrap-common-reference-exists-with<R, T, RQual<R>, TQual<T>>
&& !ref-wrap-common-reference-exists-with<T, R, TQual<T>, RQual<R>> )
struct basic_common_reference<R, T, RQual, TQual> {
using type = common_reference_t<typename R::type&, TQual<T>>;
};
template <class T, class R, template <class> class TQual, template <class> class RQual>
requires( ref-wrap-common-reference-exists-with<R, T, RQual<R>, TQual<T>>
&& !ref-wrap-common-reference-exists-with<T, R, TQual<T>, RQual<R>> )
struct basic_common_reference<T, R, TQual, RQual> {
using type = common_reference_t<typename R::type&, TQual<T>>;
};
Add the following macro definition to 17.3.2
[version.syn], header
<version>
synopsis, with
the value selected by the editor to reflect the date of adoption of this
paper:
#define __cpp_lib_common_reference 20XXXXL // also in <type_traits>
#define __cpp_lib_common_reference_wrapper 20XXXXL // also in <functional>