我遇到了一个C ++习惯,我试图研究它以了解其影响并验证其用法。但我似乎无法找到确切的答案。
std::vector< Thing > getThings();
void do() {
const std::vector< Thing > &things = getThings();
}
这里我们有一些返回非const&
值的函数。我看到的习惯是在从函数中分配返回值时使用const&
左值。提出这种习惯的理由是它减少了副本。
现在我一直在研究RVO(返回值优化),复制省略和C ++ 11移动语义。我意识到给定的编译器可以选择通过RVO 来阻止复制,无论在这里使用const&
。但是,const&
左值的使用是否会对防止副本的非const&
返回值产生任何影响?我特别询问了前C ++ 11编译器,之前移动语义。
我的假设是编译器实现了RVO,或者它没有实现RVO,并且说左值应该是const&
不会暗示或强制无副本的情况。
修改
我特别询问const&
这里的使用是否会减少副本,而不是临时对象的生命周期,如"the most important const"
进一步澄清问题
这是:
const std::vector< Thing > &things = getThings();
与此不同:
std::vector< Thing > things = getThings();
减少副本?或者它对编译器是否可以减少副本没有任何影响,例如通过RVO?
答案 0 :(得分:2)
从语义上讲,编译器需要一个可访问的拷贝构造函数,在调用站点,即使稍后,编译器也忽略了对拷贝构造函数的调用 - 优化在编译后期完成在语义分析阶段之后阶段。
阅读完评论后,我想我更了解你的问题。现在让我详细回答一下。
想象一下,该函数有这个return语句:
return items;
从语义上讲,编译器需要一个可访问的拷贝构造函数(或move-constructor),这可以省略。但是,仅仅为了争论,假设它在这里复制并且副本存储在__temp_items
中,我将其表达为:
__temp_items <= return items; //first copy:
现在在呼叫站点,假设您没有使用const &
,所以它变为:
std::vector<Thing> things = __temp_items; //second copy
现在你可以看到自己,有两个副本。允许编译器忽略它们。
但是,您的实际代码使用const &
,因此它变为:
const std::vector<Thing> & things = __temp_items; //no copy anymore.
现在,语义上只有一个副本,编译器仍然可以省略它。至于第二个副本,我不会说const&
&#34;阻止&#34; 它在编译器优化它的意义上,而不是语言允许开始
但有趣的是,无论编译器在返回时复制多少次,或者删除少数(或全部)复制,返回值 临时。如果是这样,那么如何绑定临时工作?如果这也是你的问题(现在我知道这不是你的问题但是保持这种方式,以便我不必删除我的答案的这部分),然后是的它有效并且保证用语言。
正如文章the most imporant const
中非常详细地解释的那样,如果const
引用绑定到临时,那么临时的生命周期将延长到引用范围和它与对象的类型无关。
在C ++ 11中,还有另一种延长临时生命周期的方法,即rvalue-reference:
std::vector<Thing> && things = getThings();
它具有相同的效果,但优点(或缺点 - 取决于上下文)是您也可以修改内容。
我个人更喜欢把它写成:
auto && things = getThings();
但是那不是必然一个rvalue-reference - 如果你改变了函数的返回类型,返回一个引用,那么things
就会绑定到lvalue-reference 。如果你想讨论这个问题,那就完全不同了。
答案 1 :(得分:1)
嘿所以你的问题是:
“当一个函数按值返回一个类实例,并将它分配给一个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中。
Simples。
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的情况类似,但检查总是好的。