首页 > 解决方案 > 三元运算符中是否强制复制省略(如果允许)?

问题描述

请考虑以下 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()

即使在具有优化的发布配置中。

即使没有优化,GCC 和 Clang 的输出也是不同的 ( https://gcc.godbolt.org/z/ofGrzhjbc )

S() ~S()

这里复制省略是可选的(所有编译器都在他们的权限范围内),还是这里不允许复制省略(只有 MSVC 是正确的),或者这里是强制复制省略(只有 GCC 和 Clang 是正确的)?

标签: c++c++17language-lawyercopy-elision

解决方案


条件运算符很复杂,我们必须仔细阅读标准才能理解它。见 [ expr.cond ]。

p4:“否则,如果第二个和第三个操作数具有不同的类型,并且其中一个具有(可能是 cv 限定的)类类型 [...],则尝试形成从每个操作数到类型的隐式转换序列[...] 如果E2是纯右值 [或 ...] 并且至少一个操作数具有(可能是 cv 限定的)类类型:目标类型是E2在应用左值到右值、数组到指针和函数到指针的标准转换之后将具有。使用该过程,确定是否可以形成从第二操作数到为第三操作数确定的目标类型的隐式转换序列,反之亦然。如果两个序列都可以形成,或者一个可以形成但它是模棱两可的转换序列,则程序是非良构的。如果不能形成转换序列,则操作数保持不变,并按如下所述执行进一步检查。否则,如果恰好可以形成一个转换序列,则将该转换应用于所选操作数,并且在本子条款的其余部分中使用转换后的操作数代替原始操作数。"

根据 p4,因为std::nullopt_t可以隐式转换为std::optional<S>,所以分析继续假设已完成此类转换(如果选择了第三个操作数)。到非引用目标类型的隐式转换会std::optional<S>产生一个 prvalue 类型std::optional<S>。所以对于子条款的其余部分,我们假设第二个和第三个操作数都是类型的纯右值std::optional<S>

p6:“否则,结果是纯右值。[...]”

p7:“在第二个和第三个操作数上执行左值到右值、数组到指针和函数到指针的标准转换。在这些转换之后,应满足以下条件之一:第二个和第三个操作数具有相同类型;结果属于该类型,并且使用选定的操作数初始化结果对象。[...]"

第二个和第三个都已经是纯右值,所以没有左值到右值的转换要执行。它们具有相同的类型,因此结果属于该类型。它是 的prvalue std::optional<S>

到目前为止,还不std::optional<S>需要移动。最后, 的初始化v受到保证复制省略的影响,因此那里也不会发生移动。相反,来自条件表达式的结果纯右值v作为其结果对象,因此v直接从作为条件表达式结果的纯右值“recipe”初始化。

你还没有说你使用的是什么版本的 MSVC,但它的行为对我来说似乎很奇怪。显然它没有正确实现 C++17 保证移动省略,所以假设它支持 C++14。但是在 C++14 中,这段代码应该使用 move 构造函数,它被删除了;因此,该程序应该是格式错误的。我看不出有任何理由允许它进行复制。


推荐阅读