Document #: | P2255R1 |
Date: | 2021-04-11 |
Project: | Programming Language C++ |
Audience: |
EWG LEWG |
Reply-to: |
Tim Song <[email protected]> |
This paper proposes adding two new type traits with compiler support to detect when the initialization of a reference would bind it to a lifetime-extended temporary, and changing several standard library components to make such binding ill-formed when it would inevitably produce a dangling reference. This would resolve [LWG2813].
tuple
and pair
as deleted instead of removing them from overload resolution.
Before
|
After
|
---|---|
Generic libraries, including various parts of the standard library, need to initialize an entity of some user-provided type T
from an expression of a potentially different type. When T
is a reference type, this can easily create dangling references. This occurs, for instance, when a std::tuple<const T&>
is initialized from something convertible to T
:
This construction always creates a dangling reference, because the std::string
temporary is created inside the selected constructor of tuple
(template<class... UTypes> tuple(UTypes&&...)
), and not outside it. Thus, unlike string_view
’s implicit conversion from rvalue strings, under no circumstances can this construction be correct.
Similarly, a std::function<const string&()>
currently accepts any callable whose invocation produces something convertible to const string&
. However, if the invocation produces a std::string
or a const char*
, the returned reference would be bound to a temporary and dangle.
Moreover, in both of the above cases, the problematic reference binding occurs inside the standard library’s code, and some implementations are known to suppress warnings in such contexts.
[P0932R1] proposes modifying the constraints on std::function
to prevent such creation of dangling references. However, the proposed modification is incorrect (it has both false positives and false negatives), and correctly detecting all cases in which dangling references will be created without false positives is likely impossible or at least heroically difficult without compiler assistance, due to the existence of user-defined conversions.
[CWG1696] changed the core language rules so that initialization of a reference data member in a mem-initializer is ill-formed if the initialization would bind it to a temporary expression, which is exactly the condition these traits seek to detect. However, the ill-formedness occurs outside a SFINAE context, so it is not usable in constraints, nor suitable as a static_assert
condition. Moreover, this requires having a class with a data member of reference type, which may not be suitable for user-defined types that want to represent references differently (to facilitate rebinding, for instance).
Similar to [CWG1696], we can make returning a reference from a function ill-formed if it would be bound to a temporary. Just like [CWG1696], this cannot be used as the basis of a constraint or as a static_assert
condition. Additionally, such a change requires library wording to react, as is_convertible
is currently defined in terms of such a return statement. While such a language change may be desirable, it is neither necessary nor sufficient to accomplish the goals of this paper. It can be proposed separately if desired.
During a previous EWG telecon discussion, some have suggested inventing some sort of new initialization rules, perhaps with new keywords like direct_cast
. The author of this paper is unwilling to spare a kidney for any new keyword in this area, and such a construct can easily be implemented in the library if the traits are available. Moreover, changing initialization rules is a risky endeavor; such changes frequently come with unintended consequences (for recent examples, see [gcc-pr95153] and [LWG3440]). It’s not at all clear that the marginal benefit from such changes (relative to the trait-based approach) justifies the risk.
This paper proposes two traits, reference_constructs_from_temporary
and reference_converts_from_temporary
, to cover both (non-list) direct-initialization and copy-initialization. The former is useful in classes like std::tuple
and std::pair
where explicit
constructors and conversion functions may be used; the latter is useful for INVOKE<R>
(e.g., std::function
) where only implicit conversions are considered.
As is customary in the library traits, “construct” is used to denote direct-initialization and “convert” is used to denote copy-initialization.
Unlike most library type traits, this paper proposes that the traits handle prvalues and xvalues differently: reference_converts_from_temporary<int&&, int>
is true
, while reference_converts_from_temporary<int&&, int&&>
is false
. This is useful for INVOKE<R>
; binding an rvalue reference to the result of an xvalue-returning function is not incorrect (as long as the function does not return a dangling reference itself), but binding it to a prvalue (or a temporary object materialized therefrom) would be.
INVOKE<R>
and is_invocable_r
Changing the definition of INVOKE<R>
as proposed means that is_invocable_r
will also change its meaning, and there will be cases where R v = std::invoke(args...);
is valid but is_invocable_r_v<R, decltype((args))...>
is false
:
auto f = []{ return "hello"; };
const std::string& v = std::invoke(f); // OK
static_assert(is_invocable_r_v<const std::string&, decltype((f))>); // now fails
However, we already have the reverse case today (is_invocable_r_v
is true
but the declaration isn’t valid, which is the case if R
is cv void
), so generic code already cannot use is_invocable_r
for this purpose.
More importantly, actual usage of INVOKE<R>
in the standard clearly suggests that changing its definition is the right thing to do. It is currently used in four places:
std::function
std::visit<R>
std::bind<R>
std::packaged_task
In none of them is producing a temporary-bound reference ever correct. Nor would it be correct for the proposed std::invoke_r
([P2136R1]), std::any_invocable
([P0288R7]), or std::function_ref
([P0792R5]).
tuple
/pair
constructors: deletion vs. constraintsThe wording in R0 of this paper added constraints to the constructor templates of tuple
and pair
to remove them from overload resolution when the initialization would require binding to a materialized temporary. During LEWG mailing list review, it was pointed out that this would cause the construction to fall back to the tuple(const Types&...)
constructor instead, with the result that a temporary is created outside the tuple
constructor and then bound to the reference.
While there are plausible cases where doing this is valid (for instance, f(tuple<const string&>("meow"))
, where the temporary string will live until the end of the full-expression), the risk of misuse is great enough that this revision proposes that the constructor be deleted in this scenario instead. Deleting the constructor still allows the condition to be observable to type traits and constraints, and avoids silent fallback to a questionable overload. Advanced users who desire such a binding can still explicitly convert the string themselves, which is what they have to do for correctness today anyway.
Clang has a __reference_binds_to_temporary
intrinsic that partially implements the direct-initialization variant of the proposed trait: it does not implement the part that involves reference binding to a prvalue of the same or derived type.
static_assert(__reference_binds_to_temporary(std::string const &, const char*));
static_assert(not __reference_binds_to_temporary(int&&, int));
static_assert(not __reference_binds_to_temporary(Base const&, Derived));
However, that part can be done in the library if required, by checking that
T
is a reference type;U
is not a reference type (i.e., it represents a prvalue);is_convertible_v<remove_cvref_t<U>*, remove_cvref_t<T>*>
is true
.This wording is relative to [N4868].
<type_traits>
synopsis, as indicated:namespace std { […] template<class T> struct has_unique_object_representations; + template<class T, class U> struct reference_constructs_from_temporary; + template<class T, class U> struct reference_converts_from_temporary; […] template<class T> inline constexpr bool has_unique_object_representations_v = has_unique_object_representations<T>::value; + template<class T, class U> + inline constexpr bool reference_constructs_from_temporary_v + = reference_constructs_from_temporary<T, U>::value; + template<class T, class U> + inline constexpr bool reference_converts_from_temporary_v + = reference_converts_from_temporary<T, U>::value; […] }
? For the purpose of defining the templates in this subclause, let
VAL<T>
for some typeT
be an expression defined as follows:
- (?.1) If
T
is a reference or function type,VAL<T>
is an expression with the same type and value category asdeclval<T>()
.- (?.2) Otherwise,
VAL<T>
is a prvalue that initially has typeT
. [ Note ?: IfT
is cv-qualified, the cv-qualification is subject to adjustment (7.2.2 [expr.type]). — end note ]
Template Condition Preconditions
conjunction_v<is_reference<T>, is_constructible<T, U>>
istrue
, and the initializationT t(VAL<U>);
bindst
to a temporary object whose lifetime is extended (6.7.7 [class.temporary]).
T
andU
shall be complete types, cvvoid
, or arrays of unknown bound.
conjunction_v<is_reference<T>, is_convertible<U, T>>
istrue
, and the initializationT t = VAL<U>;
bindst
to a temporary object whose lifetime is extended (6.7.7 [class.temporary]).
T
andU
shall be complete types, cvvoid
, or arrays of unknown bound.
11 Constraints:
- (11.1)
is_constructible_v<first_type, U1>
istrue
and- (11.2)
is_constructible_v<second_type, U2>
istrue
.12 Effects: Initializes
first
withstd::forward<U1>(x)
andsecond
withstd::forward<U2>(y)
.13 Remarks: The expression inside
explicit
is equivalent to:!is_convertible_v<U1, first_type> || !is_convertible_v<U2, second_type>
. This constructor is defined as deleted ifreference_constructs_from_temporary_v<first_type, U1&&>
istrue
orreference_constructs_from_temporary_v<second_type, U2&&>
istrue
.14 Constraints:
- (14.1)
is_constructible_v<first_type, const U1&>
istrue
and- (14.2)
is_constructible_v<second_type, const U2&>
istrue
.15 Effects: Initializes members from the corresponding members of the argument.
16 Remarks: The expression inside
explicit
is equivalent to:!is_convertible_v<const U1&, first_type> || !is_convertible_v<const U2&, second_type>
. This constructor is defined as deleted ifreference_constructs_from_temporary_v<first_type, const U1&>
istrue
orreference_constructs_from_temporary_v<second_type, const U2&>
istrue
.17 Constraints:
- (17.1)
is_constructible_v<first_type, U1>
istrue
and- (17.2)
is_constructible_v<second_type, U2>
istrue
.18 Effects: Initializes
first
withstd::forward<U1>(p.first)
andsecond
withstd::forward<U2>(p.second)
.19 Remarks: The expression inside
explicit
is equivalent to:!is_convertible_v<U1, first_type> || !is_convertible_v<U2, second_type>
. This constructor is defined as deleted ifreference_constructs_from_temporary_v<first_type, U1&&>
istrue
orreference_constructs_from_temporary_v<second_type, U2&&>
istrue
.template<class... Args1, class... Args2> constexpr pair(piecewise_construct_t, tuple<Args1...> first_args, tuple<Args2...> second_args);
[ Drafting note: No changes are needed here because this is a Mandates: and the initialization is ill-formed under [CWG1696]. ]
20 Mandates:
- (20.1)
is_constructible_v<first_type, Args1...>
istrue
and- (20.2)
is_constructible_v<second_type, Args2...>
istrue
.21 Effects: Initializes
first
with arguments of typesArgs1...
obtained by forwarding the elements offirst_args
and initializessecond
with arguments of typesArgs2...
obtained by forwarding the elements ofsecond_args
. (Here, forwarding an elementx
of typeU
within a tuple object means callingstd::forward<U>(x)
.) This form of construction, whereby constructor arguments forfirst
andsecond
are each provided in a separatetuple
object, is called piecewise construction.
11 Constraints:
sizeof...(Types)
equalssizeof...(UTypes)
andsizeof...(Types)
≥ 1 andis_constructible_v<Ti, Ui>
istrue
for all i.12 Effects: Initializes the elements in the tuple with the corresponding value in
std::forward<UTypes>(u)
.13 Remarks: The expression inside
explicit
is equivalent to:!conjunction_v<is_convertible<UTypes, Types>...>
. This constructor is defined as deleted if(reference_constructs_from_temporary_v<Types, UTypes&&> || ...)
istrue
.[…]
18 Constraints:
- (18.1)
sizeof...(Types)
equalssizeof...(UTypes
), and- (18.2)
is_constructible_v<Ti, const Ui&>
istrue
for all i, and- (18.3) either
sizeof...(Types)
is not 1, or (whenTypes...
expands toT
andUTypes...
expands toU
)is_convertible_v<const tuple<U>&, T>
,is_constructible_v<T, const tuple<U>&>
, andis_same_v<T, U>
are allfalse
.19 Effects: Initializes each element of
*this
with the corresponding element ofu
.20 Remarks: The expression inside
explicit
is equivalent to:!conjunction_v<is_convertible<const UTypes&, Types>...>
. This constructor is defined as deleted if(reference_constructs_from_temporary_v<Types, const UTypes&> || ...)
istrue
.21 Constraints:
- (21.1)
sizeof...(Types)
equalssizeof...(UTypes
), and- (21.2)
is_constructible_v<Ti, Ui>
istrue
for all i, and- (21.3) either
sizeof...(Types)
is not 1, or (whenTypes...
expands toT
andUTypes...
expands toU
)is_convertible_v<tuple<U>, T>
,is_constructible_v<T, tuple<U>>
, andis_same_v<T, U>
are allfalse
.22 Effects: For all i, initializes the ith element of
*this
withstd::forward<Ui>(get<i>(u))
.23 Remarks: The expression inside
explicit
is equivalent to:!conjunction_v<is_convertible<UTypes, Types>...>
. This constructor is defined as deleted if(reference_constructs_from_temporary_v<Types, UTypes&&> || ...)
istrue
.24 Constraints:
- (24.1)
sizeof...(Types)
is 2,- (24.2)
is_constructible_v<T0, const U1&>
istrue
, and- (24.3)
is_constructible_v<T1, const U2&>
istrue
.25 Effects: Initializes the first element with
u.first
and the second element withu.second
.26 Remarks: The expression inside
explicit
is equivalent to:!is_convertible_v<const U1&, T0> || !is_convertible_v<const U2&, T1>
. This constructor is defined as deleted ifreference_constructs_from_temporary_v<T0, const U1&>
istrue
orreference_constructs_from_temporary_v<T1, const U2&>
istrue
.27 Constraints:
- (27.1)
sizeof...(Types)
is 2,- (27.2)
is_constructible_v<T0, U1>
istrue
, and- (27.3)
is_constructible_v<T1, U2>
istrue
.28 Effects: Initializes the first element with
std::forward<U1>(u.first)
and the second element withstd::forward<U2>(u.second)
.29 Remarks: The expression inside
explicit
is equivalent to:!is_convertible_v<U1, T0> || !is_convertible_v<U2, T1>
. This constructor is defined as deleted ifreference_constructs_from_temporary_v<T0, U1&&>
istrue
orreference_constructs_from_temporary_v<T1, U2&&>
istrue
.
2 Define
INVOKE<R>(f, t1, t2, ... , tN )
asstatic_cast<void>(INVOKE(f, t1, t2, ... , tN ))
if R is cvvoid
, otherwiseINVOKE(f, t1, t2, ... , tN )
implicitly converted toR
. Ifreference_converts_from_temporary_v<R, decltype(INVOKE(f, t1, t2, ... , tN))>
istrue
,INVOKE<R>(f, t1, t2, ... , tN )
is ill-formed.
[CWG1696] Richard Smith. 2013-05-31. Temporary lifetime and non-static data member initializers.
https://wg21.link/cwg1696
[gcc-pr95153] Alisdair Meredith. 2020. Bug 95153 - Arrays of const void *
should not be copyable in C++20.
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=95153
[LWG2813] Brian Bi. std::function should not return dangling references.
https://wg21.link/lwg2813
[LWG3440] Ville Voutilainen. Aggregate-paren-init breaks direct-initializing a tuple or optional from {aggregate-member-value}.
https://wg21.link/lwg3440
[N4868] Richard Smith. 2020-10-18. Working Draft, Standard for Programming Language C++.
https://wg21.link/n4868
[P0288R7] Ryan McDougall, Matt Calabrese. 2020-09-03. any_invocable.
https://wg21.link/p0288r7
[P0792R5] Vittorio Romeo. 2019-10-06. function_ref: a non-owning reference to a Callable.
https://wg21.link/p0792r5
[P0932R1] Aaryaman Sagar. 2018-02-07. Tightening the constraints on std::function.
https://wg21.link/p0932r1
[P2136R1] Zhihao Yuan. 2020-05-15. invoke_r.
https://wg21.link/p2136r1