为什么std :: optional的强制转换操作符被忽略?

时间:2017-08-24 12:19:16

标签: c++ casting operator-overloading c++17 static-cast

此代码

#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。否则,如果还有一个std::optional<T>,则原则上将忽略用户类中T的每个强制转换操作符 - 即使它是完美匹配 - 。我觉得这很讨厌。

我昨天发布了一些similar question,但可能没有准确地说明我的问题,因为最终的讨论更多是关于GCC的错误。这就是我在这里再次明确要求的原因。

2 个答案:

答案 0 :(得分:4)

如果表达式中有implicit conversion sequence到所需类型,并且结果对象从表达式中direct-initialized,则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

一旦我们开始构建,我们将遵循与previous answer相同的步骤,枚举构造函数,最终选择使用operator int()的构造函数模板。

答案 1 :(得分:4)

如果Barry的优秀答案仍然不明确,这是我的版本,希望它有所帮助。

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

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

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

原因是template<typename U> optional(U&&)构造函数模板,当Tint)可以从U构造而U不是{时,它应该激活{1}}也不std::in_place_t,并从中直接初始化optional<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直接初始化int。仅在此时选择my_foo作为初始化operator int()的更好匹配。结果因此变为int

2)如果是2,虽然听起来就像“初始化my_opt = static_cast<std::optional<int>>(my_foo)一样 - 如果它是my_opt ” ,它实际上 表示 std::optional<int>创建一个临时std::optional<int>并从该中移动 - 分配” [expr.static.cast]/4

  

如果my_foo是引用类型,则效果与执行相同   声明和初始化
T对于一些发明的临时   变量T t(e);([dcl.init])然后使用临时变量作为   转换的结果。否则,结果对象是   从t直接初始化。

所以它变成了:

e

我们又回到了以前的状况; my_opt = std::optional<int>(my_foo); 随后从暂时my_opt初始化,已经持有optional

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

至于原因的原因,在提案N3793和标准中直到Nov 15, 2016确实

2

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

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

具有以下基本原理:

  

以下代码目前格式不正确(感谢STL for   引人注目的例子):

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

这是因为它需要两个用户定义的转换(来自   optional<string> opt_str = "meow"; const char*,从stringstring)   语言只允许一个。这可能是一个惊喜和一个   给用户带来不便。

     

optional<string>应该可以从任何optional<T>隐式转换   隐式可转换为U。这可以作为非显式实现   构造函数模板T,仅通过SFINAE启用   如果optional(U&&)is_convertible_v<U, T>,加上任何   避免与其他人产生歧义所需的其他条件   构造...

最后我认为is_constructible_v<T, U>排名高于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&