请考虑以下 C++17 代码:
#include <iostream>
#include <optional>
struct S
{
S(int) { std::cout << "S() "; }
S(const S &) { std::cout << "S(const S &) "; }
S(S &&) = delete;
~S() { std::cout << "~S() "; }
};
int main()
{
[[maybe_unused]] std::optional<S> v = true ? std::optional<S>(1) : std::nullopt;
}
在最新的 Visual Studio 2019 16.10.3 中,使用 /std:c++latest 选项 (C++20) 打印
S() S(const S &) ~S() ~S()
即使在经过优化的发布配置中。
即使没有优化,GCC 和 Clang 的输出也是不同的(https://gcc.godbolt.org/z/ofGrzhjbc)
S() ~S()
这里的复制省略是可选的(所有编译器都在他们的权限范围内),还是这里不允许复制省略(只有MSVC是正确的),还是这里的复制省略是强制的(只有GCC和Clang是正确的)?
条件运算符比较复杂,需要仔细阅读标准才能理解。请参阅 [expr.cond]。
p4: “否则,如果第二个和第三个操作数具有不同的类型,并且其中一个具有(可能是 cv 限定的)类类型 [...],则会尝试形成从每个操作数到该类型的隐式转换序列[...] 如果
E2
是纯右值 [或 ...] 并且至少一个操作数具有(可能是 cv 限定的)类类型:目标类型是 E2
将要执行的类型。在应用左值到右值、数组到指针和函数到指针标准转换之后,可以确定是否可以形成从第二操作数到所确定的目标类型的隐式转换序列。第三个操作数,反之亦然。
如果两个序列都可以形成,或者可以形成一个序列但它是不明确的转换序列,则该程序是错误的。
如果无法形成转换序列,则操作数保持不变,并按如下所述执行进一步检查。
否则,如果可以形成恰好一个转换序列,则将该转换应用于所选操作数,并且在本子条款的其余部分中使用转换后的操作数代替原始操作数。“
根据 p4,由于
std::nullopt_t
可以隐式转换为 std::optional<S>
,因此分析将继续假设已完成此类转换(如果选择了第三个操作数)。到非引用目标类型 std::optional<S>
的隐式转换会生成 std::optional<S>
类型的纯右值。因此,对于子条款的其余部分,我们假设第二个和第三个操作数都是 std::optional<S>
类型的纯右值。
p6: “否则,结果是纯右值。[...]”
p7: “对第二个和第三个操作数执行左值到右值、数组到指针和函数到指针的标准转换。 在这些转换之后,应满足以下条件之一: 第二个和第三个操作数具有相同的类型;结果是该类型,并且结果对象是使用选定的操作数初始化的。 [...]”
第二个和第三个都已经是纯右值,因此不需要执行左值到右值的转换。它们具有相同的类型,因此结果也是该类型。它是
std::optional<S>
的纯右值。
到目前为止,不需要任何
std::optional<S>
的动作。最后,v
的初始化受到保证复制省略的影响,因此那里也不会发生移动。相反,条件表达式的结果纯右值将 v
作为其结果对象,因此 v
直接从作为条件表达式结果的纯右值“配方”进行初始化。
你还没有说你使用的是哪个版本的MSVC,但它的行为对我来说似乎很奇怪。显然它没有正确实现 C++17 保证的移动省略,所以假设它支持 C++14。但在C++14中,这段代码应该使用move构造函数,被删除了;因此,该程序应该是格式错误的。我不明白为什么它会被允许复制。
正如接受的答案正确解释的那样,MSVC 在这里是不正确的,因为它在这种情况下没有实现 C++17 保证移动省略。
不幸的是,MSVC 的行为保持不变,从对报告问题的响应来看,该问题至少会存在一段时间:
...我们已经调查了这个问题(多次):但不幸的是,我们解决此问题的尝试多次导致构建其他项目失败 - 这些主要涉及具有非平凡析构函数的类的其他问题。
由于上述问题,我们无法承诺在接下来的几个错误修复冲刺中解决此问题,因此我们将关闭此开发者社区票证。对于此问题给您带来的不便,我们深表歉意。 ...
同时作为一种解决方法,可以用 lambda 替换问题中的三元运算符,如下所示:
auto ternary = []<class T>( bool flag, auto v1, auto v0 ) -> T {
if ( flag )
return v1;
return v0;
};
int main() {
std::optional<S> v = ternary.operator()<std::optional<S>>( true, 1, std::nullopt );
}
已被 MSVC 接受。在线演示:https://gcc.godbolt.org/z/4x9KW19n1