使用三元运算符并删除移动/复制ctor时Visual Studio不执行RVO [英] Visual Studio not performing RVO when ternary operator is used and move/copy ctors are deleted

查看:80
本文介绍了使用三元运算符并删除移动/复制ctor时Visual Studio不执行RVO的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

请看下面的代码示例,我希望它在返回值优化(RVO)的过程中执行强制复制省略,并使用C ++ 17(/ std:c ++ 17)进行编译,但是在Visual上编译时会出现错误Studio 2017(我正在使用VS17,更具体地说是15.9.8)。

 类NoCopyOrMove 
{
public:
NoCopyOrMove()=默认值;
NoCopyOrMove(int a,int b){}

NoCopyOrMove(const NoCopyOrMove&)=删除;
NoCopyOrMove& operator =(const NoCopyOrMove&)=删除;

NoCopyOrMove(NoCopyOrMove&)=删除;
NoCopyOrMove& operator =(NoCopyOrMove&)=删除;


私人:
int a,b;
};

NoCopyOrMove get(bool b)
{
return b? NoCopyOrMove(1,2):NoCopyOrMove();

//如果(b)
//返回NoCopyOrMove(1,2);

//返回NoCopyOrMove();
}

int main()
{
NoCopyOrMove m = get(true);
}

错误是:

 错误C2280:'NoCopyOrMove :: NoCopyOrMove(NoCopyOrMove&)):尝试引用已删除的函数
注意:请参见'NoCopyOrMove :: NoCopyOrMove'的声明
注意:'NoCopyOrMove :: NoCopyOrMove(NoCopyOrMove&)):函数已明确删除

注意:似乎可以在GCC上编译,并且if / else的版本都可以在上面编译,所以不确定我缺少什么。



我发现了其他一些问题发生在stackoverflow上,但是它们来自c17之前的时代,大多指的是调用复制而不是移动,因此再次询问。



基于cppreference发生复制省略:


在return语句中,当操作数是与
类型(忽略cv限定)相同类的prvalue时函数返回类型:


三元运算符的结果应为prva lue:


a吗? b:c,一些b和c的三元条件表达式(有关详细信息,请参见
定义);







编辑以使用更简单的代码:



鉴于上面的NoCopyOrMove,下面的代码也试图调用move-ctor。

  int main()
{
挥发性布尔值b = true;
NoCopyOrMove m = b NoCopyOrMove(1,2):NoCopyOrMove();
}






更新:报告链接

解决方案

是错误吗?


是。这是MSVC中的错误。几乎所有其他支持C ++ 17的编译器都对其进行编译。下面是我们产生的程序集:



所有人都使用<$ c对其进行编译$ c> -std = c ++ 17 或 -std = c ++ 1z (对于ellcc)。


标准怎么说?


条件表达式(由三元运算符形成的表达式)根据这些规则(请参阅第8.5节。 16)。


8.5.16的第1段描述了排序,第2到7部分描述了所得表达式的值类别(有关值类别的说明,请参见第8.2.1节)。



  • 第2款涵盖了第二或第三操作数均无效的情况。

  • 第3款涵盖了以下情况:第二和第三操作数都是glvalued位域(即不是prvalue)

  • 第4段介绍了第二和第三操作数具有不同类型的情况

  • 第5段涉及第二个和第三个操作数是相同类型的glvalues(也不是prvalues)的情况。

  • 第6段:



否则,结果为prvalue。如果第二个和第三个操作数不具有相同的类型,并且都具有(可能是cv限定的)类类型,则使用重载分辨率确定要应用于这些操作数的转换(如果有)(16.3.1.2、16.6) 。如果重载解析失败,则程序格式错误。否则,将应用由此确定的转换,并将转换后的操作数用于本节其余部分的原始操作数。


我们的答案。结果是prvalue,因此不需要使用copy或move构造函数,因为该值将在调用函数提供的内存中实例化(此内存位置作为 hidden参数传递给您的函数)。 / p>

您的程序是否隐式引用了move构造函数或copy构造函数?


Jon Harper友好地指出了标准指出:


程序隐式或显式地引用了已删除的函数,而不是对其进行声明。 (11.4.3.2)


这引出了一个问题:您的程序隐式引用了move构造函数还是copy构造函数?


答案是否定的。因为条件表达式的结果是prvalue,所以不会实现任何临时值,因此,既没有引用move构造函数,也没有引用copy构造函数。显式或隐式。引用 cppreference (强调我的意思):


<在以下情况下,即使复制/移动构造函数和析构函数具有明显的副作用,在以下情况下,也要求编译器忽略类对象的复制和移动构造。这些对象直接构造到存储中,否则会将它们复制/移动到其中。 复制/移动构造函数不需要存在或不可访问,因为语言规则确保即使在概念上也不会进行复制/移动操作:



  • 在return语句中,当操作数是与函数返回类型相同的类类型(忽略cv限定)的prvalue时:


    T f(){return T(); }


    f(); //只有一个对T的默认构造函数的调用



  • 在变量的初始化中,当初始化器表达式是与变量类型相同的类类型(忽略cv限定词)的prvalue:




T x = T(T(f())); //仅调用T的默认构造函数,以初始化x



区分NRVO和RVO


一个争论的源头是是否可以保证复制权限。重要的是要区分命名返回值优化和纯返回值优化。


如果返回局部变量,则不能保证。这就是返回值优化。如果您的return语句是一个prvalue表达式,则可以保证。


例如:

  NoCopyOrMove foo(){
NoCopyOrMove myVar {}; //初始化
return myVar; //错误:移动构造函数已删除
}

我正在返回一个表达式( myVar ),即自动存储对象的名称。在这种情况下,允许返回值优化,但不能保证 。该标准的第15.8.3节在此处适用。


另一方面,如果我这样写:

  NoCopyOrMove foo(){
return NoCopyOrMove(); //没有错误(C ++ 17及更高版本)
}

复制省略保证,并且不会进行复制或移动。同样,如果我写:

  NoCopyOrMove foo(); //声明foo 
NoCopyOrMove bar(){
return foo(); //返回foo返回的内容
}

仍然可以保证复制删除 ,因为 foo()的结果是一个prvalue。


结论


MSVC实际上,有一个错误。


Looking at below code sample I would expect it to perform mandatory copy elision as part of Return Value Optimization (RVO) and compile with C++17 (/std:c++17) but it compiles with an error on Visual Studio 2017 (I'm using VS17, 15.9.8 more specifically).

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 is:

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

NOTE: seems to compile on GCC and the the version with if/else compiles fine on both so not sure what I am missing.

I found a few other questions on stackoverflow, but they were from pre-c17 era and were mostly referring to "copy is called instead of move", hence asking again.

based on cppreference Copy elision happens:

In a return statement, when the operand is a prvalue of the same class type (ignoring cv-qualification) as the function return type:

and result of ternary operator should be a prvalue:

a ? b : c, the ternary conditional expression for some b and c (see definition for detail);

Any ideas why it doesn't compile?


Edit to use a simpler code:

given the NoCopyOrMove above, below code is also attempting to call move-ctor.

int main()
{
    volatile bool b = true;
    NoCopyOrMove m = b ? NoCopyOrMove(1,2) : NoCopyOrMove();
}


Update: report link

解决方案

Is it a bug?

Yes. This is a bug in MSVC. Pretty much every other compiler that supports C++17 compiles it. Below we have the assembly produced by:

And all of them compile it with -std=c++17 or -std=c++1z (for ellcc).

What does the standard say?

Conditional expressions (the ones formed by the ternary operator) produce values according to these rules (see section 8.5.16).

Paragraph 1 of 8.5.16 describes sequencing, and parts 2 through 7 describe the value category of the resulting expression (see section 8.2.1 for a description of value categories).

  • Paragraph 2 covers the case that either the second or third operands are void.
  • Paragraph 3 covers the case that both the second and third operands are glvalued bitfields (i.e., not prvalues)
  • Paragraph 4 covers the case the second and third operands have different types
  • Paragraph 5 covers the case that the second and third operands are glvalues of the same type (also not prvalues)
  • Paragraph 6:

Otherwise, the result is a prvalue. If the second and third operands do not have the same type, and either has (possibly cv-qualified) class type, overload resolution is used to determine the conversions (if any) to be applied to the operands (16.3.1.2, 16.6). If the overload resolution fails, the program is ill-formed. Otherwise, the conversions thus determined are applied, and the converted operands are used in place of the original operands for the remainder of this subclause.

This gives us our answer. The result is a prvalue, so it's unnecessary to use the copy or move constructors, as the value will be instantiated in the memory provided by the calling function (this memory location is passed as a "hidden" parameter to your function).

Does your program implicitly refer to the move constructor or the copy constructor?

Jon Harper was kind enough to point out that the standard states:

A program that refers to a deleted function implicitly or explicitly, other than to declare it, is ill-formed. (11.4.3.2)

This begs the question: does your program implicitly refer to the move constructor or the copy constructor?

The answer to this is no. Because the result of the conditional expression is a prvalue, no temporary is materialized, and as a result neither the move constructor or the copy constructor is referenced, either explicitly or implicitly. To quote cppreference (emphasis mine):

Under the following circumstances, the compilers are required to omit the copy and move construction of class objects, even if the copy/move constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. The copy/move constructors need not be present or accessible, as the language rules ensure that no copy/move operation takes place, even conceptually:

  • In a return statement, when the operand is a prvalue of the same class type (ignoring cv-qualification) as the function return type:

    T f() { return T(); }

    f(); // only one call to default constructor of T

  • In the initialization of a variable, when the initializer expression is a prvalue of the same class type (ignoring cv-qualification) as the variable type:

T x = T(T(f())); // only one call to default constructor of T, to initialize x

Distinguishing between NRVO and RVO

One source of contention is whether or not Copy Elision is guaranteed. It is important to distinguish between Named Return Value Optimization, and pure Return Value Optimization.

If you return a local variable, it's not guaranteed. This is Named Return Value Optimization. If your return statement is an expression that's a prvalue, it is guaranteed.

For example:

NoCopyOrMove foo() {
    NoCopyOrMove myVar{}; //Initialize
    return myVar; //Error: Move constructor deleted
}

I am returning a an expression (myVar) that is the name of an object of automatic storage. In this case, return value optimization is permitted but not guaranteed. Section 15.8.3 of the standard applies here.

On the other hand, if I write:

NoCopyOrMove foo() {
    return NoCopyOrMove(); // No error (C++17 and above)
}

Copy Elision is guaranteed, and no copy or move takes place. Similarly, if I write:

NoCopyOrMove foo(); //declare foo
NoCopyOrMove bar() {
    return foo(); //Returns what foo returns
}

Copy Elision is still guaranteed because the result of foo() is a prvalue.

Conclusion

MSVC does, in fact, have a bug.

这篇关于使用三元运算符并删除移动/复制ctor时Visual Studio不执行RVO的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆