为什么 std::optional 的赋值运算符在这个简单示例的编译时上下文中不可用?

Why is std::optional's assignment operator not usable in compile-time context in this simple example?

在 Compiler Explorer(以及 std::optional 上阅读 cppref.com)摆弄了半小时后,我放弃了。除了我不明白为什么这段代码不能编译之外,没什么好说的。有人请解释一下,如果有的话,也许可以告诉我一种解决方法? 我在这里使用的 std::optional 的所有成员函数都是 constexpr,并且确实应该在编译时可以计算,因为这里的可选类型 - size_t - 是原始标量类型。

#include <type_traits>
#include <optional>

template <typename T>
[[nodiscard]] constexpr std::optional<size_t> index_for_type() noexcept
{
    std::optional<size_t> index;
    if constexpr (std::is_same_v<T, int>)
        index = 1;
    else if constexpr (std::is_same_v<T, void>)
        index = 0;

    return index;
}
 
static_assert(index_for_type<int>().has_value());

https://godbolt.org/z/YKh5qT4aP

让我们简单一点:

constexpr std::optional<size_t> index_for_type() noexcept
{
    std::optional<size_t> index;
    index = 1;
    return index;
}

static_assert(index_for_type().has_value());

对于 index = 1;,您在 assignment operators 中尝试调用的候选人是 #4:

template< class U = T >
optional& operator=( U&& value );

请注意,这个候选并不是最初在 C++20 中制作的 constexpr,它是最近的 DR (P2231R1)。 libstdc++ 尚未实现此更改,这就是您的示例无法编译的原因。到目前为止,它是完全正确的 C++20 代码。图书馆还没有完全跟上。


Marek's 建议有效的原因:

constexpr std::optional<size_t> index_for_type() noexcept
{
    std::optional<size_t> index;
    index = size_t{1};
    return index;
}

static_assert(index_for_type().has_value());

是因为它没有调用赋值运算符 #4(因为同样的原因它会继续不起作用,只是在这个实现中还没有 constexpr),它切换到调用运算符 #3(即constexpr):

constexpr optional& operator=( optional&& other ) noexcept(/* see below */);

为什么是这个?因为 #4 有这个约束:

and at least one of the following is true:

  • T is not a scalar type;
  • std::decay_t<U> is not T.

这里,Tsize_t(它是optional特化的模板参数),U是参数类型。在原始情况下,index = 1Uint,这使得第二个项目符号成立(int 实际上不是 size_t),因此这个赋值运算符已验证。但是当我们把它改成index = size_t{1}时,现在U变成了size_t,所以第二个项目符号也是假的,我们失去了这个赋值运算符作为候选。

这留下了复制赋值和移动赋值作为候选,后者更好。移动分配 constexpr 在这个实现中,所以它有效。


当然,更好的实现仍然是避免赋值并且只是:

constexpr std::optional<size_t> index_for_type() noexcept
{
    return 1;
}

static_assert(index_for_type().has_value());

或者,在原函数中:

template <typename T>
[[nodiscard]] constexpr std::optional<size_t> index_for_type() noexcept
{
    if constexpr (std::is_same_v<T, int>) {
        return 1;
    } else if constexpr (std::is_same_v<T, void>) {
        return 0;
    } else {
        return std::nullopt;
    }
}

这工作得很好,即使在 C++17 中也是如此。