P2255R2: A type trait to detect reference binding to temporary

P2255R2 is a proposal for C++23 that introduces reference_constructs_from_temporary and reference_converts_from_temporary, type traits that detect when the initialization of a reference would bind it to a lifetime-extended temporary.

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 create dangling references. This occurs when a std::tuple<const T&> is initialized from a type convertible to T. When the constructor returns, the tuple will hold a dangling reference, because the temporary object is created inside the constructor, not outside it.

For example, let box<const T&> holds an object of type const T&, and it can be initialized from a type convertible to T. Because the constructor is a function template, the conversion does not occur when the constructor is called. Instead, it happens in the member initializer list. This means a temporary object of type T might be created and then destroyed at the end of the constructor. Therefore, box will hold a dangling reference when the constructor returns.

template <typename T> class box {
public:
  template <typename U>
    requires std::convertible_to<U &&, T>
  box(U &&element) : storage_(std::forward<U>(element)) {}

  T storage_;
};

template <typename T> auto consume_box(box<T>) -> void {}

auto main() -> std::int32_t {
  // undefined behavior: `box` holds a dangling reference
  box<const std::string &> string_box("hello");

  // undefined behavior: `box` holds a dangling reference
  consume_box(box<const std::string &>("hello"));
  return 0;
}

However, if the constructor is not a function template and accepts an argument of type T&&, the conversion is forced to occur when the constructor is called. This means a temporary object of type T might be created and then destroyed at the end of the full expression that includes the constructor invocation. As a result, box will end up holding a dangling reference at the end of this expression.

template <typename T> class box {
public:
  box(T &&element) : storage_(std::move(element)) {}

private:
  T storage_;
};

template <typename T> auto consume_box(box<T>) -> void {}

auto main() -> std::int32_t {
  // undefined behavior: `box` holds a dangling reference
  box<const std::string &> string_box("hello");

  // well-formed: `box` doesn't outlive the full-expression
  consume_box(box<const std::string &>("hello"));
  return 0;
}

Similarly, a std::function<const T&()> accepts a callable whose invocation produces a type convertible to T. However, the returned reference will be bound to a temporary object that gets destroyed at the end of the function, leading to a dangling reference.

Design

template <typename T, typename U> struct reference_constructs_from_temporary;
template <typename T, typename U> struct reference_converts_from_temporary;

template <typename T, typename U>
inline constexpr bool reference_constructs_from_temporary_v =
    reference_constructs_from_temporary<T, U>::value;
template <typename T, typename U>
inline constexpr bool reference_converts_from_temporary_v =
    reference_converts_from_temporary<T, U>::value;

If T is a reference or function type, VAL<T> is an expression with the same type and value category as declval<T>(). Otherwise, VAL<T> is a prvalue with type T.

  • 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.
  • 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.
static_assert(std::reference_constructs_from_temporary_v<int &&, int>);
static_assert(std::reference_constructs_from_temporary_v<const int &, int>);
static_assert(not std::reference_constructs_from_temporary_v<int &&, int &&>);
static_assert(not std::reference_constructs_from_temporary_v<const int &, int &&>);
static_assert(std::reference_constructs_from_temporary_v<int &&, long &&>);
static_assert(std::reference_constructs_from_temporary_v<int &&, long>);