C++ 以最佳方式返回复制的值

C++ 以最佳方式返回复制的值,c++,C++,我很惊讶地看到Jonathan Wakely在libstdc++中引入了以下优化:并尝试对其进行研究 对于任何非平凡类,给定concat函数的两个实现,令人惊讶的是,只有第一个是最优的(一个副本c-tor和一个运算符+=调用)。其他人需要额外的复制/移动c-tor呼叫-您可以在godbolt.org上进行验证 为什么呢?我假设所有这些实现都将使用-O2生成相同的代码 语言规范是否规定了此类行为(如果是,为什么?),或者这是一个QoI问题 这是所有版本和语言版本上GCC和Clang之间的一致行为

我很惊讶地看到Jonathan Wakely在libstdc++中引入了以下优化:并尝试对其进行研究

对于任何非平凡类,给定concat函数的两个实现,令人惊讶的是,只有第一个是最优的(一个副本c-tor和一个运算符+=调用)。其他人需要额外的复制/移动c-tor呼叫-您可以在godbolt.org上进行验证

为什么呢?我假设所有这些实现都将使用-O2生成相同的代码

语言规范是否规定了此类行为(如果是,为什么?),或者这是一个QoI问题

这是所有版本和语言版本上GCC和Clang之间的一致行为

void extern_copy();
void extern_move();
void extern_plus_assign();

struct myclass
{
    myclass(const myclass&)
    {
        extern_copy();
    }
    myclass(myclass&&)
    {
        extern_move();
    }
    myclass& operator+=(const myclass&)
    {
        extern_plus_assign();
        return *this;
    }
};

myclass concat(const myclass& lhs, const myclass& rhs)
{
    myclass copy(lhs);
    copy += rhs;
    return copy;
}


myclass concat2(const myclass& lhs, const myclass& rhs)
{
    myclass copy(lhs);
    return copy += rhs;
}

myclass concat3(const myclass& lhs, const myclass& rhs)
{
    return myclass(lhs) += rhs;
}

static myclass concat4impl(myclass lhs, const myclass& rhs)
{
    return lhs += rhs;
}

myclass concat4(const myclass& lhs, const myclass& rhs)
{
    return concat4impl(lhs, rhs);
}


static myclass concat5impl(myclass lhs, const myclass& rhs)
{
    lhs += rhs;
    return lhs;
}

myclass concat5(const myclass& lhs, const myclass& rhs)
{
    return concat5impl(lhs, rhs);
}
更新:修改代码以排除以下运算符+=”实现的问题:

myclass& myclass::operator+=(myclass const& v) {
  static myclass weird;
  return weird;
}
考虑这一点:

myclass& myclass::operator+=(myclass const& v) {
  static myclass weird;
  return weird;
}
这完全合法

您的推理基于我们认为理所当然的一点:+=运算符将始终返回
*this

编译器不允许做出这种假设

concat()
中,这无关紧要,因为我们强制
concat()
的返回值为+=操作的
myclass
的实例。在所有其他情况下,编译器必须假设情况更糟,除非它可以保证+=返回<代码> *< < /> > .< /p> 复制删除(RVO,NRVO,和一个特殊的初始化情况)是一个非常特殊的优化:它是C++中唯一没有被AS-IF规则覆盖的优化。对于每一次其他优化,编译器只需以一种不改变可观察行为、只改变性能的方式修改程序。但是RVO可以改变可观察的行为,因为省略的复制构造函数和析构函数调用(如输出)中的副作用会丢失。这就是为什么标准中有一个特殊的许可证,允许编译器在非常特定的情况下执行此优化

具体来说,RVO仅在返回值为prvalue时适用,NRVO仅在返回表达式引用局部变量时适用

语句
returncopy+=rhs中的返回表达式两者都不是。它并不是简单地引用局部变量(它是一个复合赋值表达式,而不是id表达式),返回值也不是prvalue(您的
+=
重载返回一个左值引用,使该值成为左值;如果运算符按值返回,这将是不同的,但否则将非常不规则)


您可能认为编译器可以内联运算符并发现它是同一个对象,但在此级别上没有授予执行此优化的权限。

我记得最近观看的一次演讲中,在
return语句中需要复制省略
语句
是局部变量的名称,您可以通过键入
return(x)来阻止它
而不是
返回x。不幸的是,我记不起是哪一次谈话了,我也不能给出任何其他好的参考。我想是这一次:不,不是这一次。如果将myclass更改为以下,则会得到相同的行为:`void extern_copy();无效外部移动();无效外部加上赋值();结构myclass{myclass();myclass(const myclass&){extern_copy();}myclass(myclass&){extern_move();}myclass&operator=(const myclass&);myclass&operator=(myclass&);myclass&operator+=(const myclass&){extern_plus_assign();返回*this;}`@Maciejcancora啊,我很惊讶编译器没能解决这个问题。潜在的问题仍然是引用不是prvalues保持不变。是否要详细说明“引用不是prvalues”问题?在CutAT5函数<代码> CutAT5IMPL(LHS,RHS)< /Cord>表达式为PROVALL(右?),如果我正确理解,这个行为由C++标准强制执行。您是否看到RVO/NRVO规则无法扩展到上述情况的原因?(返回copy+=rhs),这将需要进行跨函数分析,并对单独的编译和可能的编译时间造成严重破坏。我不明白为什么它会像您所说的那样有问题。如果编译器有可用的函数定义,那么它们已经执行了跨函数分析(例如内联),并且由于RVO/NRVO已经是可选的,所以当函数定义不可用时,它们就不能执行此优化。@MaciejConcora它们这样做是为了优化,而不是为了语言规则。从C++17开始,复制省略是强制性的。不,编译器现在不/不能执行这种优化,因为它们不允许按语言规则进行优化(优化不能改变可观察的行为,RVO/NRVO是该规则的例外)。当表达式
返回copy+=rhs时,C++17不改变任何内容