为什么三元运算符会阻止返回值优化?

13

为什么在MSVC中三元运算符会防止返回值优化(RVO)?请考虑以下完整的示例程序:

#include <iostream>

struct Example
{
    Example(int) {}
    Example(Example const &) { std::cout << "copy\n"; }
};

Example FunctionUsingIf(int i)
{
    if (i == 1)
        return Example(1);
    else
        return Example(2);
}

Example FunctionUsingTernaryOperator(int i)
{
    return (i == 1) ? Example(1) : Example(2);
}

int main()
{
    std::cout << "using if:\n";
    Example obj1 = FunctionUsingIf(0);
    std::cout << "using ternary operator:\n";
    Example obj2 = FunctionUsingTernaryOperator(0);
}
< p>使用VC 2013编译的方式如下:cl /nologo /EHsc /Za /W4 /O2 stackoverflow.cpp

输出:

using if:
using ternary operator:
copy

显然,三元运算符会阻止RVO,为什么?为什么编译器不能聪明地看到使用三元运算符的函数和使用if语句的函数执行相同的操作,并进行相应的优化?


2
可能是编译器的问题。GCC不会导致复制: http://coliru.stacked-crooked.com/a/ab6969a2cb4b499a - tmaric
4
我会使用return Example(i == 1 ? 1 : 2);,并怀疑这样做可以实现RVO。 - MSalters
5
在这种情况下,如果Example有两个构造函数,参数类型不同,那么这将不是一个选项。 - James Kanze
@James:更加奇怪的是,使用 /Od(“禁用优化”)编译得到的结果也是相同的... - Christian Hackl
@JamesKanze:毫不意外。它在return Example(i == 1 ? Example(1) : Example("two"))周围变得有趣。(虽然你不想要,但是可以看看编译器如何处理RVO) - MSalters
显示剩余3条评论
3个回答

5
看程序输出,我觉得编译器在两种情况下都省略了,为什么?
因为如果没有启用省略,正确的输出应该是:
1. 在函数返回时构造示例对象; 2. 将其复制到临时对象; 3. 将临时对象复制到主函数中定义的对象。
因此,我期望屏幕上至少有2个“copy”输出。确实,如果我使用g++编译并带有-fno-elide-constructor选项执行您的程序,则从每个函数中获得2个复制消息。
有趣的是,如果我使用clang执行相同的操作,则在调用函数FunctionUsingTernaryOperator(0);时会得到3个“copy”消息,我猜这是由于编译器如何实现三元运算符所致。我猜它正在生成一个临时变量来解决三元运算符,并将此临时变量复制到返回语句。

如果我用这段代码(使用一些虚拟整数创建temp)替换三元运算符,那么在VC开启优化后输出中就不会有任何“复制”。因此,至少在VC中存在一些差异。原始问题仍然存在。 - Christian Hackl
通过优化,它实际上在编译时解决了三元运算符。运行时代码只是3个对std::cout<<的调用。如果发生了@ChristianHackl正在寻找的省略,则只会有2个对std::cout<<的调用。 - jliv902

2

这个相关问题包含了答案。

标准规定在哪些情况下可以允许拷贝或移动构造函数被省略,如在返回语句中:(12.8.31)

  • 在带有类返回类型的函数的返回语句中, 当表达式是与函数返回类型同种非易失自动对象(除了函数或catch子句参数)的名称时,通过直接构造自动对象到函数的返回值中来省略复制/移动操作。
  • 当一个未被绑定到引用的临时类对象(12.2)将被拷贝/移动到具有相同cv-unqualified类型的类对象时,通过将临时对象直接构造到省略的复制/移动目标中来省略复制/移动操作。

因此,基本上仅在以下情况下发生拷贝省略:

  1. 返回命名对象。
  2. 返回临时对象

如果您的表达式既不是命名对象也不是临时对象,则会退回到复制。

一些有趣的行为:

  • return (name);不会防止复制省略(参见此问题)
  • return true?name:name;应该防止复制省略,但至少在gcc 4.6上是错误的(cf.这个问题)

编辑:

我保留了原来的答案,但Christian Hackl在他的评论中是正确的,它没有回答问题。

就规则而言,例子中的三元运算符产生了一个临时对象,因此12.8.31允许省略复制/移动。因此从C ++语言的角度来看。编译器完全可以在从FunctionUsingTernaryOperator返回时省略拷贝。

现在显然没有进行省略。我想唯一的原因是Visual Studio编译器团队尚未实现它。并且因为理论上他们可以,在将来的版本中他们可能会这样做。


这可能只是另一种情况,我意识到我的C ++理解有多么有限,但我认为一个命名对象只是临时对象(=无名对象)的相反。更确切地说,我认为我的代码示例中的三元运算符将产生一个临时对象,因为它没有名称。如果你所说的是正确的,那么其他未命名但非临时对象的示例是什么?(顺便说一句,我认为你在引用旧的C ++标准; 在C ++11中,措辞和段落编号有些改变。) - Christian Hackl
@ChristianHackl 我编辑了我的回答,考虑到了你的评论。 - Arnaud
@ChristianHackl 我正在使用标准的N3337版本。这是C++11标准发布后的第一个版本,基本上是对官方标准中的错字进行了修正。你在使用哪个版本? - Arnaud
你赢了,我这里只有一个草稿版本,显然比你引用的要旧。 - Christian Hackl

0

我可以看出它违反了RVO的一个通用规则 - 返回对象应该在单个位置定义。

下面的代码片段符合该规则:

Example e;
e = (i == 1)? Example{1} : Example{2};
return e;

但是在原始表达式中,根据MSVC的解析,两个Example对象在两个不同的位置被定义:
return (i == 1) ? Example(1) : Example(2);

尽管将这两个代码片段之间的转换对于人类来说非常简单,但我可以想象,在没有专门的实现的情况下,编译器不会自动进行转换。换句话说,这是一个技术上可通过返回值优化(RVO)处理的特殊情况,但开发人员并没有意识到。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接