| Document #: | P2255R1 |
| Date: | 2021-04-11 |
| Project: | Programming Language C++ |
| Audience: |
EWG LEWG |
| Reply-to: |
Tim Song <t.canens.cpp@gmail.com> |
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_rChanging 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 failsHowever, 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::functionstd::visit<R>std::bind<R>std::packaged_taskIn 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 typeTbe an expression defined as follows:
- (?.1) If
Tis 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 ?: IfTis 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>);bindstto a temporary object whose lifetime is extended (6.7.7 [class.temporary]).
TandUshall 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>;bindstto a temporary object whose lifetime is extended (6.7.7 [class.temporary]).
TandUshall be complete types, cvvoid, or arrays of unknown bound.
11 Constraints:
- (11.1)
is_constructible_v<first_type, U1>istrueand- (11.2)
is_constructible_v<second_type, U2>istrue.12 Effects: Initializes
firstwithstd::forward<U1>(x)andsecondwithstd::forward<U2>(y).13 Remarks: The expression inside
explicitis 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&&>istrueorreference_constructs_from_temporary_v<second_type, U2&&>istrue.14 Constraints:
- (14.1)
is_constructible_v<first_type, const U1&>istrueand- (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
explicitis 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&>istrueorreference_constructs_from_temporary_v<second_type, const U2&>istrue.17 Constraints:
- (17.1)
is_constructible_v<first_type, U1>istrueand- (17.2)
is_constructible_v<second_type, U2>istrue.18 Effects: Initializes
firstwithstd::forward<U1>(p.first)andsecondwithstd::forward<U2>(p.second).19 Remarks: The expression inside
explicitis 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&&>istrueorreference_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...>istrueand- (20.2)
is_constructible_v<second_type, Args2...>istrue.21 Effects: Initializes
firstwith arguments of typesArgs1...obtained by forwarding the elements offirst_argsand initializessecondwith arguments of typesArgs2...obtained by forwarding the elements ofsecond_args. (Here, forwarding an elementxof typeUwithin a tuple object means callingstd::forward<U>(x).) This form of construction, whereby constructor arguments forfirstandsecondare each provided in a separatetupleobject, is called piecewise construction.
11 Constraints:
sizeof...(Types)equalssizeof...(UTypes)andsizeof...(Types)≥ 1 andis_constructible_v<Ti, Ui>istruefor all i.12 Effects: Initializes the elements in the tuple with the corresponding value in
std::forward<UTypes>(u).13 Remarks: The expression inside
explicitis 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&>istruefor all i, and- (18.3) either
sizeof...(Types)is not 1, or (whenTypes...expands toTandUTypes...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
*thiswith the corresponding element ofu.20 Remarks: The expression inside
explicitis 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>istruefor all i, and- (21.3) either
sizeof...(Types)is not 1, or (whenTypes...expands toTandUTypes...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
*thiswithstd::forward<Ui>(get<i>(u)).23 Remarks: The expression inside
explicitis 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.firstand the second element withu.second.26 Remarks: The expression inside
explicitis 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&>istrueorreference_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
explicitis 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&&>istrueorreference_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