为什么 std::optional 的转换运算符会被忽略?

Why is a cast operator to std::optional ignored?

这个代码

#include <iostream>
#include <optional>

struct foo
{
    explicit operator std::optional<int>() {
        return std::optional<int>( 1 );
    }
    explicit operator int() {
        return 2;
    }
};

int main()
{
    foo my_foo;

    std::optional<int> my_opt( my_foo );
    std::cout << "constructor: " << my_opt.value() << std::endl;

    my_opt = static_cast<std::optional<int>>(my_foo);
    std::cout << "static_cast: " << my_opt.value() << std::endl;
}

produces the following output

constructor: 2
static_cast: 2

在 Clang 4.0.0 和 MSVC 2017 (15.3) 中。 (让我们暂时忽略 GCC,因为在这种情况下它的行为似乎是 buggy。)

为什么输出是2?我希望 1std::optional 的构造函数似乎更喜欢转换为内部类型 (int),尽管转换为外部类型 (std::optional<int>) 是可用的。根据 C++ 标准,这是正确的吗?如果是这样,标准是否有理由不规定更喜欢尝试投射到外部类型?我会发现这更合理,并且可以想象如果可以转换为外部类型,则可以使用 enable_ifis_convertible 来禁用 ctor 来实现它。否则,用户 class 中每个转换为 std::optional<T> 的运算符 - 即使它是完美匹配 - 如果还有一个转换为 T,原则上也会被忽略。我会觉得这很讨厌。

我昨天发布了一些 但可能没有准确说明我的问题,因为由此产生的讨论更多是关于 GCC 错误。这就是为什么我在这里更明确地再次询问。

如果表达式中有 implicit conversion sequence from the expression to the desired type, and the resulting object is direct-initialized

A static_cast 有效。所以写:

my_opt = static_cast<std::optional<int>>(my_foo);

遵循与以下相同的步骤:

std::optional<int> __tmp(my_foo); // direct-initialize the resulting
                                  // object from the expression
my_opt = std::move(__tmp);        // the result of the cast is a prvalue, so move

一旦开始构建,我们将按照与我的 相同的步骤,枚举构造函数,最终选择使用 operator int().

的构造函数模板

如果 Barry 的出色回答仍然不清楚,这是我的版本,希望对您有所帮助。

最大的问题是为什么在直接初始化中不首选用户定义的 optional<int> 转换:

    std::optional<int> my_opt(my_foo);

毕竟有一个构造函数optional<int>(optional<int>&&)和一个用户定义的my_foooptional<int>的转换。

原因是 template<typename U> optional(U&&) 构造函数模板,它应该在 T (int) 可从 U 构造并且 Ustd::in_place_toptional<T> 都没有,直接从中初始化 T。确实如此,消除了 optional(foo&).

最终生成的 optional<int> 看起来像:

class optional<int> {
    . . .
    int value_;
    . . .
    optional(optional&& rhs);
    optional(foo& rhs) : value_(rhs) {}
    . . .

optional(optional&&) 需要用户定义的转换,而 optional(foo&)my_foo 的精确匹配。所以它赢了,并直接从 my_foo 初始化 int。只有在此时才选择 operator int() 作为更好的匹配来初始化 int。结果因此变成 2.

2) 在 my_opt = static_cast<std::optional<int>>(my_foo) 的情况下,尽管它 听起来 像“ 初始化 my_opt 就好像它是 std::optional<int>",它实际上 的意思是 "从 [=21 创建一个临时的 std::optional<int> =] 并从 " 移动赋值,如 [expr.static.cast]/4:

中所述

If T is a reference type, the effect is the same as performing the declaration and initialization
T t(e); for some invented temporary variable t ([dcl.init]) and then using the temporary variable as the result of the conversion. Otherwise, the result object is direct-initialized from e.

所以变成:

    my_opt = std::optional<int>(my_foo);

我们又回到了以前的状态; my_opt 随后从临时 optional 初始化,已经持有 2.

转发引用的超载问题是众所周知的。 Scott Myers 在他的书 Effective Modern C++ 的第 26 章中广泛讨论了为什么在 "universal references" 上重载是个坏主意。这样的模板会不知疲倦地消除你扔给它们的任何类型,这将掩盖一切和任何不完全匹配的东西。所以我很惊讶委员会选择了这条路线。


至于为什么会这样,提案N3793 and in the standard until Nov 15, 2016中确实是

  optional(const T& v);
  optional(T&& v);

但后来作为 LWG defect 2451 的一部分,它变成了

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

理由如下:

Code such as the following is currently ill-formed (thanks to STL for the compelling example):

optional<string> opt_str = "meow";

This is because it would require two user-defined conversions (from const char* to string, and from string to optional<string>) where the language permits only one. This is likely to be a surprise and an inconvenience for users.

optional<T> should be implicitly convertible from any U that is implicitly convertible to T. This can be implemented as a non-explicit constructor template optional(U&&), which is enabled via SFINAE only if is_convertible_v<U, T> and is_constructible_v<T, U>, plus any additional conditions needed to avoid ambiguity with other constructors...

最后我觉得 T 的排名比 optional<T> 还好,毕竟这是 可能 的一个相当不寻常的选择一个值和 值。

在性能方面,从 T 而不是另一个 optional<T> 进行初始化也是有益的。 optional 通常实现为:

template<typename T>
struct optional {
    union
    {
        char dummy;
        T value;
    };
    bool has_value;
};

所以从 optional<T>& 初始化它看起来像

optional<T>::optional(const optional<T>& rhs) {
  has_value = rhs.has_value;
  if (has_value) {
    value = rhs.value;
  }
}

而从 T& 初始化需要的步骤更少:

optional<T>::optional(const T& t) {
  value = t;
  has_value = true;
}