c++ - 使用三元运算符并删除移动/复制 ctor 时,Visual Studio 不执行 RVO
问题描述
查看下面的代码示例,我希望它作为返回值优化 (RVO) 的一部分执行强制复制省略并使用 C++17 (/std:c++17) 编译,但它在 Visual Studio 2017 上编译时出现错误(我正在使用 VS17,更具体地说是 15.9.8)。
class NoCopyOrMove
{
public:
NoCopyOrMove() = default;
NoCopyOrMove(int a, int b){}
NoCopyOrMove(const NoCopyOrMove&) = delete;
NoCopyOrMove& operator=(const NoCopyOrMove&) = delete;
NoCopyOrMove(NoCopyOrMove&&) = delete;
NoCopyOrMove& operator=(NoCopyOrMove&&) = delete;
private:
int a, b;
};
NoCopyOrMove get(bool b)
{
return b ? NoCopyOrMove(1,2) : NoCopyOrMove();
//if (b)
// return NoCopyOrMove(1, 2);
//return NoCopyOrMove();
}
int main()
{
NoCopyOrMove m = get(true);
}
错误是:
error C2280: 'NoCopyOrMove::NoCopyOrMove(NoCopyOrMove &&)': attempting to reference a deleted function
note: see declaration of 'NoCopyOrMove::NoCopyOrMove'
note: 'NoCopyOrMove::NoCopyOrMove(NoCopyOrMove &&)': function was explicitly deleted
注意:似乎在 GCC 上编译并且带有 if/else 的版本在两者上都编译得很好,所以不确定我错过了什么。
我在stackoverflow上发现了其他一些问题,但它们来自c17之前的时代,主要是指“调用复制而不是移动”,因此再次询问。
基于 cppreference 复制省略发生:
在 return 语句中,当操作数是与函数返回类型相同的类类型(忽略 cv 限定)的纯右值时:
三元运算符的结果应该是prvalue:
一个 ?b : c,一些 b 和 c 的三元条件表达式(详见定义);
任何想法为什么它不编译?
编辑以使用更简单的代码:
鉴于上面的 NoCopyOrMove,下面的代码也试图调用 move-ctor。
int main()
{
volatile bool b = true;
NoCopyOrMove m = b ? NoCopyOrMove(1,2) : NoCopyOrMove();
}
更新:报告链接
解决方案
这是一个错误吗?
是的。这是 MSVC 中的一个错误。几乎所有其他支持 C++17 的编译器都会编译它。下面我们的程序集由:
- ellcc https://godbolt.org/z/PfzDTs
- gcc https://godbolt.org/z/oXpDyk
- 铿锵https://godbolt.org/z/KX99Yc
- power64 AT12.0 https://godbolt.org/z/XvWiEa
- icc 19.0.1 https://godbolt.org/z/pZWBJ5
并且它们都用-std=c++17
or编译它-std=c++1z
(对于 ellcc)。
标准是怎么说的?
条件表达式(由三元运算符形成的表达式)根据这些规则产生值(参见第 8.5.16 节)。
8.5.16 的第 1 段描述了排序,第 2 到 7 部分描述了结果表达式的值类别(有关值类别的描述,请参见第 8.2.1 节)。
- 第 2 段涵盖了第二个或第三个操作数无效的情况。
- 第 3 段涵盖了第二个和第三个操作数都是泛左值位域(即,不是纯右值)的情况
- 第 4 段涵盖了第二个和第三个操作数具有不同类型的情况
- 第 5 段涵盖了第二个和第三个操作数是相同类型的左值(也不是纯右值)的情况
- 第 6 段:
否则,结果为纯右值。如果第二个和第三个操作数的类型不同,并且任何一个都具有(可能是 cv 限定的)类类型,则使用重载决议来确定要应用于操作数(16.3.1.2、16.6)的转换(如果有) . 如果重载决议失败,则程序格式错误。否则,将应用如此确定的转换,并使用转换后的操作数代替本子条款其余部分的原始操作数。
这给了我们我们的答案。结果是一个纯右值,因此不需要使用复制或移动构造函数,因为该值将在调用函数提供的内存中实例化(此内存位置作为“隐藏”参数传递给您的函数)。
您的程序是否隐式引用移动构造函数或复制构造函数?
Jon Harper 非常友好地指出标准规定:
隐式或显式引用已删除函数(而不是声明它)的程序是格式错误的。(11.4.3.2)
这就引出了一个问题:您的程序是隐式引用移动构造函数还是复制构造函数?
答案是否定的。因为条件表达式的结果是纯右值,所以没有临时物化,因此无论是显式还是隐式都没有引用移动构造函数或复制构造函数。引用cppreference(强调我的):
在以下情况下,编译器必须省略类对象的复制和移动构造,即使复制/移动构造函数和析构函数具有可观察到的副作用。对象直接构建到存储中,否则它们将被复制/移动到。复制/移动构造函数不需要存在或可访问,因为语言规则确保不会发生复制/移动操作,即使在概念上也是如此:
在 return 语句中,当操作数是与函数返回类型相同的类类型(忽略 cv 限定)的纯右值时:
T f() { return T(); }
f(); // only one call to default constructor of T
在变量的初始化中,当初始化表达式是与变量类型相同的类类型(忽略 cv-qualification)的纯右值时:
T x = T(T(f())); // only one call to default constructor of T, to initialize x
NRVO 和 RVO 的区别
争论的来源之一是复制省略是否得到保证。区分命名返回值优化和纯返回值优化很重要。
如果您返回局部变量,则无法保证。这称为返回值优化。如果您的 return 语句是一个纯右值的表达式,那么它是有保证的。
例如:
NoCopyOrMove foo() {
NoCopyOrMove myVar{}; //Initialize
return myVar; //Error: Move constructor deleted
}
我正在返回一个表达式 ( myVar
),它是自动存储对象的名称。在这种情况下,返回值优化是允许的,但不能保证。该标准的第 15.8.3 节适用于此。
另一方面,如果我写:
NoCopyOrMove foo() {
return NoCopyOrMove(); // No error (C++17 and above)
}
复制省略是有保证的,不会发生复制或移动。同样,如果我写:
NoCopyOrMove foo(); //declare foo
NoCopyOrMove bar() {
return foo(); //Returns what foo returns
}
复制省略仍然得到保证,因为结果foo()
是纯右值。
结论
事实上,MSVC 确实有一个错误。
推荐阅读
- qt - 在 Qt 快速程序中从 gpsd 获取位置
- javascript - Node.js - 没有检测到相同的数据
- swift - SwiftUI - 我如何知道何时单击了导航视图的后退按钮?
- python - 我可以在 CircuitPython 中将 i2c LCD 的地址设置为 20 以外的地址吗?
- linux - 如何删除具有相同前缀的最旧的 n 组文件?
- ios - 如何在 Swift 中更改 UIDatePicker 的颜色?
- c# - 如何将包含特定变量的元素分成单独的列表?
- amazon-web-services - 如何在 Arduino、Web 应用程序和 AWS 之间进行通信?
- php - php foreach 外循环
- c# - 无法追踪“索引超出范围”异常的原因