A type trait to detect reference binding to temporary

Document #: P2255R1
Date: 2021-04-11
Project: Programming Language C++
Audience: EWG
LEWG
Reply-to: Tim Song
<>

1 Abstract

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].

2 Revision history

3 In brief

Before
After
std::tuple<const std::string&> x("hello");  // dangling
std::function<const std::string&()> f = [] { return ""; }; // OK

f();                                        // dangling
std::tuple<const std::string&> x("hello");   // ill-formed
std::function<const std::string&()> f = [] { return ""; }; // ill-formed

4 Motivation

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:

std::tuple<const std::string&> t("meow");

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).

5 Design decisions

5.1 Alternative approaches

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.

5.2 Two type traits

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.

5.3 Treat prvalues as distinct from xvalues

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.

5.4 Changing 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:

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]).

5.5 tuple/pair constructors: deletion vs. constraints

The 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.

6 Implementation experience

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

7 Wording

This wording is relative to [N4868].

  1. Edit 20.15.3 [meta.type.synop], header <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;

   […]
 }
  1. In 20.15.5.4 [meta.unary.prop], add the following after p4:

? For the purpose of defining the templates in this subclause, let VAL<T> for some type T 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 as declval<T>().
  • (?.2) Otherwise, VAL<T> is a prvalue that initially has type T. Note ?: If T is cv-qualified, the cv-qualification is subject to adjustment (7.2.2 [expr.type]). — end note ]
  1. In 20.15.5.4 [meta.unary.prop], Table 49 [tab:meta.unary.prop], add the following:
Template Condition Preconditions
template<class T, class U>
struct reference_constructs_from_temporary;

conjunction_v<is_reference<T>, is_constructible<T, U>> is true, and the initialization T t(VAL<U>); binds t to a temporary object whose lifetime is extended (6.7.7 [class.temporary]).

T and U shall be complete types, cv void, or arrays of unknown bound.

template<class T, class U>
struct reference_converts_from_temporary;

conjunction_v<is_reference<T>, is_convertible<U, T>> is true, and the initialization T t = VAL<U>; binds t to a temporary object whose lifetime is extended (6.7.7 [class.temporary]).

T and U shall be complete types, cv void, or arrays of unknown bound.

  1. Edit 20.4.2 [pairs.pair] as indicated:
template<class U1, class U2> constexpr explicit(see below) pair(U1&& x, U2&& y);

11 Constraints:

  • (11.1) is_constructible_v<first_type, U1> is true and
  • (11.2) is_constructible_v<second_type, U2> is true.

12 Effects: Initializes first with std::forward<U1>(x) and second with std::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 if reference_constructs_from_temporary_v<first_type, U1&&> is true or reference_constructs_from_temporary_v<second_type, U2&&> is true.

template<class U1, class U2> constexpr explicit(see below) pair(const pair<U1, U2>& p);

14 Constraints:

  • (14.1) is_constructible_v<first_type, const U1&> is true and
  • (14.2) is_constructible_v<second_type, const U2&> is true.

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 if reference_constructs_from_temporary_v<first_type, const U1&> is true or reference_constructs_from_temporary_v<second_type, const U2&> is true.

template<class U1, class U2> constexpr explicit(see below) pair(pair<U1, U2>&& p);

17 Constraints:

  • (17.1) is_constructible_v<first_type, U1> is true and
  • (17.2) is_constructible_v<second_type, U2> is true.

18 Effects: Initializes first with std::forward<U1>(p.first) and second with std::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 if reference_constructs_from_temporary_v<first_type, U1&&> is true or reference_constructs_from_temporary_v<second_type, U2&&> is true.

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...> is true and
  • (20.2) is_constructible_v<second_type, Args2...> is true.

21 Effects: Initializes first with arguments of types Args1... obtained by forwarding the elements of first_args and initializes second with arguments of types Args2... obtained by forwarding the elements of second_args. (Here, forwarding an element x of type U within a tuple object means calling std::forward<U>(x).) This form of construction, whereby constructor arguments for first and second are each provided in a separate tuple object, is called piecewise construction.

  1. Edit 20.5.3.1 [tuple.cnstr] as indicated:
template<class... UTypes> constexpr explicit(see below) tuple(UTypes&&... u);

11 Constraints: sizeof...(Types) equals sizeof...(UTypes) and sizeof...(Types) ≥ 1 and is_constructible_v<Ti, Ui> is true 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&&> || ...) is true.

[…]

template<class... UTypes> constexpr explicit(see below) tuple(const tuple<UTypes...>& u);

18 Constraints:

  • (18.1) sizeof...(Types) equals sizeof...(UTypes), and
  • (18.2)is_constructible_v<Ti, const Ui&> is true for all i, and
  • (18.3) either sizeof...(Types) is not 1, or (when Types... expands to T and UTypes... expands to U) is_convertible_v<const tuple<U>&, T>, is_constructible_v<T, const tuple<U>&>, and is_same_v<T, U> are all false.

19 Effects: Initializes each element of *this with the corresponding element of u.

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&> || ...) is true.

template<class... UTypes> constexpr explicit(see below) tuple(tuple<UTypes...>&& u);

21 Constraints:

  • (21.1) sizeof...(Types) equals sizeof...(UTypes), and
  • (21.2)is_constructible_v<Ti, Ui> is true for all i, and
  • (21.3) either sizeof...(Types) is not 1, or (when Types... expands to T and UTypes... expands to U) is_convertible_v<tuple<U>, T>, is_constructible_v<T, tuple<U>>, and is_same_v<T, U> are all false.

22 Effects: For all i, initializes the ith element of *this with std::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&&> || ...) is true.

template<class U1, class U2> constexpr explicit(see below) tuple(const pair<U1, U2>& u);

24 Constraints:

  • (24.1) sizeof...(Types) is 2,
  • (24.2) is_constructible_v<T0, const U1&> is true, and
  • (24.3) is_constructible_v<T1, const U2&> is true.

25 Effects: Initializes the first element with u.first and the second element with u.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 if reference_constructs_from_temporary_v<T0, const U1&> is true or reference_constructs_from_temporary_v<T1, const U2&> is true.

template<class U1, class U2> constexpr explicit(see below) tuple(pair<U1, U2>&& u);

27 Constraints:

  • (27.1) sizeof...(Types) is 2,
  • (27.2) is_constructible_v<T0, U1> is true, and
  • (27.3) is_constructible_v<T1, U2> is true.

28 Effects: Initializes the first element with std::forward<U1>(u.first) and the second element with std::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 if reference_constructs_from_temporary_v<T0, U1&&> is true or reference_constructs_from_temporary_v<T1, U2&&> is true.

  1. Edit 20.14.4 [func.require] p2 as indicated:

2 Define INVOKE<R>(f, t1, t2, ... , tN ) as static_cast<void>(INVOKE(f, t1, t2, ... , tN )) if R is cv void, otherwise INVOKE(f, t1, t2, ... , tN ) implicitly converted to R. If reference_converts_from_temporary_v<R, decltype(INVOKE(f, t1, t2, ... , tN))> is true, INVOKE<R>(f, t1, t2, ... , tN ) is ill-formed.

8 References

[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