@Jarod42 already pointed out the core reason why this code compiles, but I will elaborate a bit.
The following two constructor templates for std::optional<T>
are relevant to this question:
template <class U>
constexpr optional(const optional<U>& other)
requires std::is_constructible_v<T, const U&>; // 1
template <class U>
constexpr optional(optional<U>&& other);
requires std::is_constructible_v<T, U>; // 2
Note that the requires-clauses above are for exposition only. They might not be present in the actual declarations provided by the library implementation. However, the standard requires constructor templates 1 and 2 to only participate in overload resolution when the corresponding std::is_constructible_v
constraint is satisfied.
The second overload will not participate in overload resolution because std::reference_wrapper<const int>
is not constructible from int
(meaning an rvalue of type int
), which is the feature that is intended to prevent dangling references. However, the first overload will participate, because std::reference_wrapper<const int>
is constructible from const int&
(meaning an lvalue of type const int
). The problem is that, when U
is deduced and the std::optional<int>
rvalue is bound to the const optional<U>&
constructor argument, its rvalueness is "forgotten" in the process.
How might this issue be avoided in a user-defined optional
template? I think it's possible, but difficult. The basic idea is that you would want a constructor of the form
template <class V>
constexpr optional(V&& other)
requires (is_derived_from_optional_v<std::remove_cvref_t<V>> && see_below) // 3
where the trait is_derived_from_optional
detects whether the argument type is a specialization of optional
or has an unambiguous base class that is a specialization of optional
. Then,
- If
V
is an lvalue reference, constructor 3 has the additional constraint that the constraints of constructor 1 above must be satisfied (where U
is the element type of the optional
).
- If
V
is not a reference (i.e., the argument is an rvalue), then constructor 3 has the additional constraint that the constraints of constructor 2 above must be satisfied, where the argument is const_cast<std::remove_cv_t<V>&&>(other)
.
Assuming the constraints of constructor 3 are satisfied, it delegates to constructor 1 or 2 depending on the result of overload resolution. (In general, if the argument is a const
rvalue, then constructor 1 will have to be used since you can't move from a const
rvalue. However, the constraint above will prevent this from occurring in the dangling reference_wrapper
case.) Constructors 1 and 2 would need to be made private and have a parameter with a private tag type, so they wouldn't participate in overload resolution from the user's point of view. And constructor 3 might also need a bunch of additional constraints so that its overload resolution priority relative to the other constructors (not shown) is not higher than that of constructors 1 and 2. Like I said, it's not a simple fix.