为什么 std::optional 的强制转换运算符被忽略了

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

本文关键字:转换 运算符 被忽略了 std optional 为什么      更新时间:2023-10-16

此代码

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

生成以下输出

constructor: 2
static_cast: 2

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

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

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

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

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

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

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

原因是template<typename U> optional(U&&)构造函数模板,当T(int)可以从U构造并且U既不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完全匹配。所以它赢了,并从my_foo直接初始化int.只有在此时,operator int()才会选择为初始化int的更好匹配项。结果因此变得2

2)在my_opt = static_cast<std::optional<int>>(my_foo)的情况下,虽然听起来像">初始化my_opt好像它是std::optional<int>",但它实际上意味着">my_foo创建一个临时std::optional<int>并从中移动分配",如[expr.static.cast]/4中所述:

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

所以它变成了:

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

我们又回到了以前的情况;my_opt随后从一个临时optional初始化,已经持有一个2

转发引用上的重载问题是众所周知的。斯科特·迈尔斯(Scott Myers)在他的著作《有效的现代C++》第26章中广泛讨论了为什么在"通用参考"上超载是一个坏主意。这样的模板将不知疲倦地消除您扔给它们的任何类型的内容,这将使所有不完全匹配的东西黯然失色。所以我很惊讶委员会选择了这条路线。


至于为什么会这样,在提案N3793和标准中直到2016年11月15日确实是

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

但后来作为 LWG 缺陷 2451 的一部分,它被更改为

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

理由如下:

如下代码目前格式不正确(感谢 STL 令人信服的例子):

optional<string> opt_str = "meow";

这是因为它需要两个用户定义的转换(从const char*string,以及从stringoptional<string>),其中 语言只允许一种。这可能是一个惊喜和一个 给用户带来不便。

optional<T>应该可以从任何U隐式转换 隐式转换为T。这可以实现为非显式 构造函数模板optional(U&&),仅通过 SFINAE 启用 如果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&初始化需要的步骤更少:

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

如果存在从表达式到所需类型的隐式转换序列,并且生成的对象是从表达式直接初始化的,则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().