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

Is copy elision mandatory (if allowed at all) in the ternary operator?

请考虑以下 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++ 最新选项 (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 限定的)class 类型 [...] 则尝试形成从这些操作数中的每一个到另一个操作数的类型的隐式转换序列。[...]如果E2是纯右值[或...]并且至少有一个操作数具有(可能是cv限定的) class 类型:目标类型是 E2 在应用左值到右值、数组到指针和函数到指针标准转换后的类型。使用这个过程,它确定是否可以形成从第二个操作数到为第三个操作数确定的目标类型的隐式转换序列,反之亦然。 如果两个序列都可以形成,或者一个可以形成但它是有歧义的转换序列,则程序格式错误。 如果无法形成转换序列,则操作数保持不变,并按如下所述进行进一步检查。 否则,如果恰好可以形成一个转换序列,则将转换应用于所选操作数,并使用转换后的操作数代替原始操作数来完成本节的其余部分。" =44=]

根据 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构造函数,被删除了;因此,程序应该是病式的。我看不出有任何理由允许它进行复制。