Search code examples
c++dangling-pointerstdoptional

prevent initializing std::optional<std::reference_wrapper<const T>> with rvalue std::optional<T>


std::reference_wrapper cannot be bound to rvalue reference to prevent dangling pointer. However, with combination of std::optional, it seems that rvalue could be bound.

That is, std::is_constructible_v<std::reference_wrapper<const int>, int&&>) is false but std::is_constructible_v<std::optional<std::reference_wrapper<const int>>, std::optional<int>&&> is true.

Here's an example:

#include <iostream>
#include <optional>

auto make() -> std::optional<int>
{
    return 3;
}

int main()
{
    std::optional<std::reference_wrapper<const int>> opt = make();
    if (opt)
        std::cout << opt->get() << std::endl;
    return 0;
}

I expected this code will be rejected by compiler, but it compiles well and opt contains dangling pointer.

Is this a bug of standard library? Or, is it just not possible to prevent dangling pointer here because of some kind of limitaion of C++ language specification?

If it is a bug of standard library, how can I fix it when I implement my own optional type?

It it's a limitation of current C++ specification, could you tell me where this problem comes from?


Solution

  • @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.