三元运算符中是否强制复制省略如果允许的话?

问题描述

请考虑以下 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;
}

在带有 /std:c++latest 选项 (C++20) 的最新 Visual Studio 2019 16.10.3 中打印

S() S(const S &) ~S() ~S()

即使在经过优化的 Release 配置中。

即使没有优化,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 中,这段代码应该使用移动构造函数,它被删除了;因此,该程序应该是格式错误的。我看不出有任何理由允许它进行复制。