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>>
istrue
and the initializationT t(VAL<U>)
bindst
to a temporary object whose lifetime is extended.reference_converts_from_temporary
: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.
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>);