阻止使用右值 std::optional<t<T 初始化 std::optional:reference_wrapper<const >>T>

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

提问人:slyx 提问时间:11/25/2022 更新时间:11/25/2022 访问量:236

问:

std::reference_wrapper不能绑定到右值引用以防止指针悬空。 但是,通过组合,似乎可以绑定右值。std::optional

也就是说,是但 是.std::is_constructible_v<std::reference_wrapper<const int>, int&&>)falsestd::is_constructible_v<std::optional<std::reference_wrapper<const int>>, std::optional<int>&&>true

下面是一个示例:

#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;
}

我预计这段代码会被编译器拒绝,但它编译得很好,并且包含悬空指针。opt

这是标准库的错误吗?或者,由于 C++ 语言规范的某种限制,无法阻止此处悬空指针吗?

如果是标准库的bug,在实现自己的类型时如何修复?optional

这是当前 C++ 规范的限制,你能告诉我这个问题来自哪里吗?

C++ 悬空指针 std可选

评论

2赞 Marek R 11/25/2022
这不是一个错误。代码无效,但问题不在于 std,而在于此示例中。当有人使用时,他应该意识到后果并小心处理。std::reference_wrapper
1赞 Jarod42 11/25/2022
我会说仍然可行,而不参与过载解决。template<class U> constexpr optional(const optional<U>&)template<class U> constexpr optional(optional<U>&&)
0赞 Marek R 11/25/2022
另一方面,clang 抱怨:godbolt.org/z/xovxT5Ezo - 更正 godbolt.org/z/McTM1h74f
0赞 Jarod42 11/25/2022
@MarekR:使用 clang 14,但不再使用 clang 15...
2赞 Brian Bi 11/25/2022
@MarekR 如果添加了第一个链接,您的第一个链接也会编译。#include <functional>

答:

2赞 Ranoiaetep 11/25/2022 #1

允许代码的原因是,当无法使用 rvalue 引用初始化 的基础对象时,它将回退到将基础对象作为 .optionalconst&


事实上,即使是这个代码也是允许的:

auto ref = std::cref(static_cast<const int&>(1));
3赞 Brian Bi 11/25/2022 #2

@Jarod42已经指出了这段代码编译的核心原因,但我将详细阐述一下。

以下两个构造函数模板与此问题相关:std::optional<T>

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

请注意,上面的 requires-clause 仅用于说明。它们可能不存在于库实现提供的实际声明中。但是,该标准要求构造函数模板 1 和 2 仅在满足相应约束时参与重载解决。std::is_constructible_v

第二个重载将不参与重载解析,因为无法构造自 (表示 类型的 rvalue ),这是旨在防止悬空引用的功能。但是,第一个重载参与,因为 is 可构造自 (表示类型的左值)。问题在于,当推导并且右值绑定到构造函数参数时,它的右值性在此过程中被“遗忘”。std::reference_wrapper<const int>intintstd::reference_wrapper<const int>const int&const intUstd::optional<int>const optional<U>&

如何在用户定义的模板中避免此问题?我认为这是可能的,但很难。基本思想是,您需要表单的构造函数optional

template <class V>
constexpr optional(V&& other)
requires (is_derived_from_optional_v<std::remove_cvref_t<V>> && see_below)  // 3

其中 trait 检测参数类型是 的专用化,还是具有 的明确基类,该基类是 的专用化。然后is_derived_from_optionaloptionaloptional

  • 如果是左值引用,则构造函数 3 具有必须满足上述构造函数 1 的约束的附加约束(其中 是 的元素类型)。VUoptional
  • 如果不是引用(参数是右值),则构造函数 3 具有必须满足上述构造函数 2 的约束的附加约束,其中参数为 。Vconst_cast<std::remove_cv_t<V>&&>(other)

假设满足构造函数 3 的约束,则根据重载解决的结果,它将委托给构造函数 1 或 2。(通常,如果参数是右值,则必须使用构造函数 1,因为您不能从右值移动。但是,上述约束将防止这种情况在悬空的情况下发生。构造函数 1 和 2 需要设为私有,并且具有具有私有标记类型的参数,因此从用户的角度来看,它们不会参与重载解析。构造函数 3 可能还需要一堆额外的约束,以便其相对于其他构造函数(未显示)的重载解析优先级不高于构造函数 1 和 2 的重载解析优先级。就像我说的,这不是一个简单的解决方案。constconstreference_wrapper