首页 > 解决方案 > 使用三元运算符并删除移动/复制 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();
}

更新:报告链接

标签: c++visual-studioc++17

解决方案


这是一个错误吗?

是的。这是 MSVC 中的一个错误。几乎所有其他支持 C++17 的编译器都会编译它。下面我们的程序集由:

并且它们都用-std=c++17or编译它-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 确实有一个错误。


推荐阅读