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

Time:11-25

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?

CodePudding user response:

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

CodePudding user response:

The reason your code is allowed is because when the underlying object of the optional cannot be initialized with rvalue reference, it will fallback to passing the underlying object as const&.


In fact even this code is allowed:

auto ref = std::cref(static_cast<const int&>(1));
  • Related