【问题标题】:const reference to temporary vs. return value optimization对临时与返回值优化的 const 引用
【发布时间】:2023-03-30 15:18:01
【问题描述】:

我知道将右值分配给 const 左值引用会延长临时对象的生命周期,直到作用域结束。但是,我不清楚何时使用它以及何时依赖返回值优化。

LargeObject lofactory( ... ) {
     // construct a LargeObject in a way that is OK for RVO/NRVO
}

int main() {
    const LargeObject& mylo1 = lofactory( ... ); // using const&
    LargeObject mylo2 = lofactory( ... ); // same as above because of RVO/NRVO ?
}

根据 Scot Meyers 的“更有效的 C++”(第 20 条),编译器可以优化第二种方法以在适当的位置构造对象(这将是理想的,并且正是第一个尝试使用 const& 实现的目标方法)。

  1. 是否有任何普遍接受的规则或最佳做法,何时将const& 用于临时人员以及何时依赖 RVO/NRVO?
  2. 会不会出现使用const&方法比不使用更糟糕的情况? (例如,如果 LargeObject 实现了 C++11 移动语义……)

【问题讨论】:

    标签: c++ optimization return-value-optimization temporary-objects const-reference


    【解决方案1】:

    让我们考虑最简单的情况:

    lofactory( ... ).some_method();
    

    在这种情况下,从 lofactory 到调用者上下文的副本是可能的 - 但它可以通过 RVO/NRVO 优化掉。


    LargeObject mylo2 ( lofactory( ... ) );
    

    在这种情况下,可能的副本是:

    1. 临时lofactory 返回到调用者上下文 - 可以通过 RVO/NRVO 优化掉
    2. temporary 复制构造 mylo2 - 可以通过 copy-elision 优化掉

    const LargeObject& mylo1 = lofactory( ... );
    

    在这种情况下,仍然可以复制一份:

    1. lofactorytemporary 返回到调用者上下文 - 可以通过 RVO/NRVO 优化掉(也是!)

    一个引用将绑定到这个临时。


    所以,

    是否有任何普遍接受的规则或最佳实践何时使用 const& 来临时以及何时依赖 RVO/NRVO?

    正如我上面所说,即使在 const& 的情况下,也可能会出现不必要的副本,并且可以通过 RVO/NRVO 对其进行优化。

    如果您的编译器在某些情况下应用 RVO/NVRO,那么它很可能会在第 2 阶段(如上)执行复制省略。因为在这种情况下,复制省略比 NRVO 简单得多。

    但是,在最坏的情况下,const& 的情况下您将拥有一份副本,而当您初始化值时,您将拥有两份。

    会不会有使用 const& 方法比不使用更糟糕的情况?

    我不认为有这样的情况。至少除非你的编译器使用了奇怪的规则来区分const&。 (举个类似情况的例子,我注意到 MSVC 不做 NVRO 来进行聚合初始化。)

    (例如,如果 LargeObject 实现了这些,我正在考虑 C++11 移动语义......)

    在 C++11 中,如果 LargeObject 具有移动语义,那么在最坏的情况下,const& 的情况下会移动一次,初始化值时会移动两次。所以,const& 还是好一点的。


    因此,如果可能的话,一个好的规则是始终将临时对象绑定到 const&,因为如果编译器由于某种原因未能执行复制省略,它可能会阻止复制?

    在不知道应用程序的实际上下文的情况下,这似乎是一个很好的规则。

    在 C++11 中,可以将临时绑定到右值引用 - LargeObject&&。所以,这样的临时性是可以修改的。


    顺便说一句,移动语义仿真可通过不同的技巧在 C++98/03 中使用。例如:

    但是,即使存在移动语义 - 也有无法廉价移动的对象。例如,内部带有双数据[4][4] 的 4x4 矩阵类。因此,即使在 C++11 中,复制省略 RVO/NRVO 仍然非常重要。顺便说一句,当复制省略/RVO/NRVO 发生时 - 它比移动更快。


    P.S.,在实际情况下,还有一些额外的事情需要考虑:

    例如,如果您有返回向量的函数,即使应用了 Move/RVO/NRVO/Copy-Elision - 它仍然可能不是 100% 有效的。例如,考虑以下情况:

    while(/*...*/)
    {
        vector<some> v = produce_next(/* ... */); // Move/RVO/NRVO are applied
        // ...
    }
    

    将代码更改为:

    vector<some> v;
    while(/*...*/)
    {
        v.clear();
    
        produce_next( v ); // fill v
        // or something like:
        produce_next( back_inserter(v) );
        // ...
    }
    

    因为在这种情况下,当 v.capacity() 足够时,可以重新使用 vector 中已分配的内存,而无需在每次迭代时在 producer_next 中进行新的分配。

    【讨论】:

    • 因此,如果可能的话,一个好的规则是总是将临时对象绑定到const&amp;,因为如果编译器无法对某些对象执行复制省略,它可能会阻止复制原因?
    【解决方案2】:

    如果你这样写lofactory 类:

    LargeObject lofactory( ... ) {
        // figure out constructor arguments to build a large object
        return { arg1, arg2, arg3 }  //  return statement with a braced-init-list
    }
    

    在这种情况下,没有 RVO/NRVO,它是直接构造。标准的第 6.6.3 节说“带有 braced-init-listreturn 语句通过复制列表初始化 (8.5.4) 初始化要从函数返回的对象或引用从指定的初始化列表中。”

    那么,如果你用

    捕获你的对象
    LargeObject&& mylo = lofactory( ... ); 
    

    不会有任何复制,生命周期如你所愿,你可以修改mylo。

    保证所有内容都不会在任何地方复制。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-02-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-20
      • 1970-01-01
      • 2015-05-23
      相关资源
      最近更新 更多