【问题标题】:Does const ref lvalue to non-const func return value specifically reduce copies?const ref lvalue 到 non-const func 返回值是否专门减少副本?
【发布时间】:2023-04-10 03:08:02
【问题描述】:

我遇到了一个 C++ 习惯,我试图研究它以了解它的影响并验证它的用法。但我似乎找不到确切的答案。

std::vector< Thing > getThings();

void do() {
    const std::vector< Thing > &things = getThings();   
}

这里我们有一些函数返回一个非const&amp; 值。我看到的习惯是在从函数分配返回值时使用const&amp; lvalue。提出这种习惯的原因是它减少了副本。

现在我一直在研究 RVO(返回值优化)、复制省略和 C++11 移动语义。我意识到给定的编译器可以选择通过 RVO 不管在此处使用 const&amp; 来阻止复制。但是在防止复制方面,这里使用const&amp; 左值对非const&amp; 返回值有什么影响吗?我特别询问的是 C++11 之前的编译器,before 移动语义。

我的假设是编译器要么实现 RVO,要么不实现,并且说左值应该是 const&amp; 并不暗示或强制实现无副本情况。

编辑

我特别询问const&amp; 的使用是否会减少副本,而不是关于临时对象的生命周期,如"the most important const" 中所述

进一步澄清问题

这是:

const std::vector< Thing > &things = getThings();

与此不同:

std::vector< Thing > things = getThings();

在减少副本方面?还是对编译器是否可以减少副本没有任何影响,比如通过RVO?

【问题讨论】:

  • @Nawaz,我也看到了这篇文章,但我被字符串文字的使用抛出了。这实际上是否完全适用于我的示例,您在堆栈上建立一个向量并返回它?这是否意味着向量没有被复制?
  • 保证将引用绑定到相同类型的值(忽略 const-qualifiers),不会复制值
  • 似乎不清楚您在问什么:您的问题似乎是在明确询问T const &amp;x = y(); 是否意味着未复制返回值,但人们已经回答了这个问题,然后您说您确实意识到它是尚未复制,似乎在问别的问题。
  • 感谢@M.M 和 Nawaz 的耐心等待。很抱歉,即使我尽力具体化,我的问题最终还是含糊其辞。
  • 按照惯例,ref-to-const 使代码脆弱,因为随着代码的维护,它可能会绑定到一个或两行后变得无效的引用结果在代码中。要了解它是否能为您带来任何好处,请衡量。很可能它根本不会提高效率:它只是一种引入一些难以启动且晦涩难懂的脆弱性的方法。

标签: c++ reference constants


【解决方案1】:

从语义上讲,编译器在调用站点需要一个可访问复制构造函数,即使稍后,编译器也会忽略对复制构造函数的调用——优化是在编译的后期完成的语义分析阶段之后的阶段。

在阅读了您的 cmets 之后,我想我更好地理解了您的问题。现在让我详细回答一下。

想象一下这个函数有这个返回语句:

return items;

从语义上讲,编译器需要这里有一个可访问的复制构造函数(或移动构造函数),可以省略。但是,为了争论,假设它在这里制作了一个副本,并且该副本存储在__temp_items中,我将其表示为:

__temp_items <= return items; //first copy: 

现在在调用点,假设你没有使用过const &amp;,所以变成了这样:

std::vector<Thing> things = __temp_items;  //second copy

现在您可以看到自己,有两个副本。编译器可以省略它们。

但是,您的实际代码使用const &amp;,所以它变成了这样:

const std::vector<Thing> & things = __temp_items;  //no copy anymore.

现在,语义上只有一个副本,编译器仍然可以省略它。至于第二个副本,我不会说const&amp;“prevented”,因为编译器已经对其进行了优化,而是语言一开始就不允许。


但有趣的是,无论编译器在返回时复制多少次,或者忽略其中的少数(或全部),返回值一个临时的。如果是这样,那么绑定到临时工作如何?如果这也是你的问题(现在我知道这不是你的问题,但请保持这种方式,这样我就不必删除我的答案的这一部分),那么是的,它可以工作,这是可以保证的由语言。

正如文章the most imporant const 中详细解释的那样,如果const 引用绑定到临时对象,那么临时对象的生命周期会延长到引用的范围,它与对象的类型

在 C++11 中,还有另一种方法可以延长临时对象的生命周期,即右值引用:

std::vector<Thing> && things = getThings();    

它具有相同的效果,但优点(或缺点 - 取决于上下文)是您还可以修改内容。

我个人更喜欢这样写:

auto && things = getThings();   

但那不一定是右值引用——如果你改变函数的返回类型来返回一个引用,那么things就会绑定到左值引用。如果你想讨论这个,那就是完全不同的话题了。

【讨论】:

  • 在该链接的 cmets 下方,有这样的内容:“根据标准,当将右值分配给 const 引用时,允许编译器复制右值。因此,无论编译器是否利用它,复制构造函数都必须是可访问的。这是否意味着您不能保证不会有副本?
  • @jdi:这些是语义问题。关键是,无论编译器在返回时复制多少次,或者忽略其中的少数,返回值 is 是一个 temporary;就生命而言,这是唯一的问题。是的,如果复制构造函数是inaccesible(比如声明为private),那么你的代码将不会编译开始。希望能澄清您的疑问。
  • 但我的问题特别是关于通过使用const&amp; 来防止复制,而不是关于生成的临时文件的生命周期。鉴于此,这个答案仍然正确吗?
  • 好的。我不明白你的问题。让我编辑我的答案以包括这一点。
  • @oldrinb:什么是“右值引用类型声明”?我想你的意思是转发参考?这是 C++14 引入的一个新术语,并不必然表示右值引用
【解决方案2】:

嘿,所以你的问题是:

“当一个函数按值返回一个类实例,并将它分配给一个 const 引用时,是否避免了复制构造函数调用?”

忽略临时的生命周期,因为这不是您要问的问题,我们可以通过查看汇编输出来了解会发生什么。我正在使用 clang,llvm 7.0.2。

这里有一些盒子标准。按值返回,没什么花哨的。

测试 A

class MyClass
{
public:
    MyClass();
    MyClass(const MyClass & source);
    long int m_tmp;
};

MyClass createMyClass();

int main()
{
    const MyClass myClass = createMyClass();
    return 0;
}

如果我使用“-O0 -S -fno-elide-constructors”编译,我会得到这个。

_main:
    pushq   %rbp                    # Boiler plate
    movq    %rsp, %rbp              # Boiler plate
    subq    $32, %rsp               # Reserve 32 bytes for stack frame
    leaq    -24(%rbp), %rdi         # arg0 = &___temp_items = rdi = rbp-24
    movl    $0, -4(%rbp)            # rbp-4 = 0, no idea why this happens
    callq   __Z13createMyClassv     # createMyClass(arg0)
    leaq    -16(%rbp), %rdi         # arg0 = & myClass
    leaq    -24(%rbp), %rsi         # arg1 = &__temp_items
    callq   __ZN7MyClassC1ERKS_     # MyClass::MyClass(arg0, arg1)
    xorl    %eax, %eax              # eax = 0, the return value for main
    addq    $32, %rsp               # Pop stack frame
    popq    %rbp                    # Boiler plate
    retq

我们只查看调用代码。我们对 createMyClass 的实现不感兴趣。那是在别处编译的。 因此 createMyClass 在一个临时对象中创建类,然后将其复制到 myClass 中。

简单。

const ref 版本呢?

测试 B

class MyClass
{
public:
    MyClass();
    MyClass(const MyClass & source);
    long int m_tmp;
};

MyClass createMyClass();

int main()
{
    const MyClass & myClass = createMyClass();
    return 0;
}

相同的编译器选项。

_main:                              # Boiler plate
    pushq   %rbp                    # Boiler plate
    movq    %rsp, %rbp              # Boiler plate
    subq    $32, %rsp               # Reserve 32 bytes for the stack frame
    leaq    -24(%rbp), %rdi         # arg0 = &___temp_items = rdi = rbp-24
    movl    $0, -4(%rbp)            # *(rbp-4) = 0, no idea what this is for
    callq   __Z13createMyClassv     # createMyClass(arg0)
    xorl    %eax, %eax              # eax = 0, the return value for main
    leaq    -24(%rbp), %rdi         # rdi = &___temp_items
    movq    %rdi, -16(%rbp)         # &myClass = rdi = &___temp_items;
    addq    $32, %rsp               # Pop stack frame
    popq    %rbp                    # Boiler plate
    retq

没有复制构造函数,因此更优化对吗?

如果我们为两个版本都关闭“-fno-elide-constructors”会发生什么?仍然保持-O0。

测试 A

_main:
    pushq   %rbp                    # Boiler plate
    movq    %rsp, %rbp              # Boiler plate
    subq    $16, %rsp               # Reserve 16 bytes for the stack frame
    leaq    -16(%rbp), %rdi         # arg0 = &myClass = rdi = rbp-16
    movl    $0, -4(%rbp)            # rbp-4 = 0, no idea what this is
    callq   __Z13createMyClassv     # createMyClass(arg0)
    xorl    %eax, %eax              # eax = 0, return value for main
    addq    $16, %rsp               # Pop stack frame
    popq    %rbp                    # Boiler plate
    retq

Clang 已删除复制构造函数调用。

测试 B

_main:                              # Boiler plate
    pushq   %rbp                    # Boiler plate
    movq    %rsp, %rbp              # Boiler plate
    subq    $32, %rsp               # Reserve 32 bytes for the stack frame
    leaq    -24(%rbp), %rdi         # arg0 = &___temp_items = rdi = rbp-24
    movl    $0, -4(%rbp)            # rbp-4 = 0, no idea what this is
    callq   __Z13createMyClassv     # createMyClass(arg0)
    xorl    %eax, %eax              # eax = 0, return value for main
    leaq    -24(%rbp), %rdi         # rdi = &__temp_items
    movq    %rdi, -16(%rbp)         # &myClass = rdi
    addq    $32, %rsp               # Pop stack frame
    popq    %rbp                    # Boiler plate
    retq

测试 B(分配给 const 引用)与以前相同。它现在比测试 A 有更多的指令。

如果我们将优化设置为 -O1 会怎样?

_main:
    pushq   %rbp                    # Boiler plate
    movq    %rsp, %rbp              # Boiler plate
    subq    $16, %rsp               # Reserve 16 bytes for the stack frame
    leaq    -8(%rbp), %rdi          # arg0 = &___temp_items = rdi = rbp-8
    callq   __Z13createMyClassv     # createMyClass(arg0)
    xorl    %eax, %eax              # ex = 0, return value for main
    addq    $16, %rsp               # Pop stack frame
    popq    %rbp                    # Boiler plate
    retq

当使用 -O1 编译时,两个源文件都会变成这个。 它们产生完全相同的汇编程序。 -O4 也是如此。

编译器不知道 createMyClass 的内容,所以它不能做更多的优化。

使用我正在使用的编译器,分配给 const ref 不会获得性能提升。

我想 g++ 和 intel 的情况类似,尽管检查总是很好。

【讨论】:

  • 这太棒了,直接回答了我的问题,而不是谈论生命周期。我非常感谢提供的具体证明并分解了汇编输出的含义!
  • 请注意,编译器发出的机器指令不是语言规范要求的,而是规范要求的总和 + 规范允许(但不是必需)的内容。所以正是因为后者,不同的编译器可以产生不同的机器指令,但仍然符合语言规范。
  • @Nawaz,我明白你在说什么。这取决于人们是否想考虑现代编译器在实践中的常见情况与语言规范所保证的内容。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-04-22
  • 2012-04-26
  • 1970-01-01
  • 1970-01-01
  • 2019-04-01
  • 1970-01-01
  • 2023-03-08
相关资源
最近更新 更多