C语言中的指针的错误使用,是很多内存错误的根源。C++中引入了_引用(reference)_,来化解指针的毒。但是引用并不是一剂完全的解毒药,还有好多毒还不能解。
下面这个毒就是个例子:
int& f() {
int a = 1;
return a;
}
上面的代码,函数f()
中返回了自身局部变量a
的引用。这是相当危险的行为,当f()
返回时,相应的栈上的内存消解了,局部变量也随之而亡。所以,使用f()
的返回值是未定义的行为,轻则出现计算错误(获取到的值非预期值),重则出现程序错误(segmental fault)。
什么是为定义的行为?简单地说,C++把自己处理不了的情况都叫做为定义行为。未定义的行为越多,也就是在抽象完备性上存在漏洞,就会转化为编程时候的坑。
一个靠谱的编译器,会为上述行为提出一个告警:
main.cc:4:10: warning: reference to stack memory associated with local variable 'a' returned [-Wreturn-stack-address]
return a;
^
1 warning generated.
我们或许可以得出一个简单的结论,返回函数局部变量的引用,是不对的。
即使是C++11新增的右值引用(rvalue reference)也是不行的。
所以,对于局部变量,我们要勇敢地返回它的值,即便它是一个很臃肿的局部变量……
返回值优化
如果我们返回的局部变量很臃肿,是一个很大的结构体,该怎么办?总的来说,有两个办法
- 编译器帮你优化
- 自己手动优化
先假设我们有一个很大的类,叫做Foo
:
struct Foo {
int i;
// 假设这里有许许多多你看不见的属性
}
接着我们修改我们的f()
,使他返回Foo
:
Foo f() {
Foo a;
a.i = 101;
return a;
}
由于Foo
很大,当我们使用f()
的返回值,比如Foo foo = f();
的时候,按照常理,f()
的返回值会被赋予foo
,这个过程可能会发生拷贝,导致性能下降。这时候靠谱的编译器会自动进行返回值优化,避免这个拷贝。这个聪明的动作叫做Return Value Optimization。这就是所谓的编译器自动优化。
那总有些情况,编译器是无法优化的,只好靠手动优化了。
使用const引用
我们可以把foo
变成函数返回值的引用,比如:const Foo& foo = f();
。由于是引用,所以就避免了拷贝。这里有几点需要注意。
第一,引用必须是静态的,也就是必须用const
来修饰这个引用。这是因为函数的返回值属于右值,也就是(rvalue)。普通的引用是左值引用,也就是lvalue reference。左值引用不能指向右值引用,只有const的左值引用才能用来指向右值。
所谓右值,简单的说就是不能放在等号左边的值。比如这个表达式是非法的:
f().i = 110;
。
本来f()
的返回值是一个临时的变量,在它调用结束后,就应该销毁了。可是通过像这样const Foo& foo = f();
,把临时的返回值赋值给一个const左值引用,f()
返回值并不会立即销毁。这等于是在const引用的作用域内,延长了f()
返回值的存活时间。
使用右值引用
C++11新增了一个引用类型,那就是右值引用(rvalue reference)。那什么是右值引用?这个似乎解释起来有点困难,顾名思义,右值引用是专门指向右值的引用(有点废话)。那什么是右值?前面说了,等号左边的是左值,那等号右边的是右值吧?好像也不对,因为一个变量也可以出现在等号右边,赋值给另外一个变量。好吧,到底什么是右值?更准确的说,不能放在等号左边的,就是右值。就像1234
这种字面值,或者前面提到的函数f()
的返回值,这些都是不能放到等号左边的。
C++11引入的右值引用,是为了作为补充,和既有的左值引用有所区别。另外右值引用是C++11的特性,所以编译的时候要加上
-std=c++11
呢。
右值引用的写法是&&
,所以可以把const Foo& foo = f();
改写成右值引用形式:const Foo&& foo = f();
。这样做看起来好像没有多大差别!那再改一下,把const去掉:Foo&& foo = f();
。这就是右值的好处,不加const就可以之间指向右值,而且可以对右值进行更改,比如:foo.i = 122;
。
再谈一下右值引用和左值引用的关系和差别:右值引用本身是一个左值,所以左值引用可以指向一个右值引用(晕菜了吧!),这意味着下面的写法是可行的:
int k = 10;
int&& i = 1;
i++; // 右值引用是左值,可以加加
i = k; // 右值引用是左值,可以重新复制
同时也因为着下面的写法是不可行的:
int&& i = 1;
int&& j = i;
原因是右值引用是左值,所以一个右值引用不能被另外一个右值引用所指向。
还是默念前面说过的那句话,能出现在等号左边的是左值,不能出现在等号右边的是右值。
偷天换日
上面说了这么多,你应该能看出来右值引用不是让你平时随便把玩的,它肩负着一个特殊的使命,那就是偷天换日。上面写到对于这个语句Foo foo = f();
,编译器会自动优化,直接用f()
的返回值当作foo
。但是,在foo
太复杂的情况,或者是编译器不够聪明的情况下,优化有可能不会进行。这时候默认的行是是调用Foo的拷贝构造函数,用f()
返回的临时变量来拷贝构造 foo
。我们的假设是Foo很复杂,所以这是个很大的工程,非常消耗性能。但这里有一点可以优化,也就是f()
返回的是临时变量,不会有其他人使用了,何不将其的内容直接搬移过来使用,说不定可以节省许多在拷贝构造中所需要的内存分配和释放的操作。这个偷天换日的任务就交给右值引用了。
首先,偷天换日之前有一个准备工作要做,那就是定义一个搬移构造函数(move constructor,也叫移动构造函数)。和拷贝构造函数Foo(Foo&)
不一样的是,搬移构造函数接受的是一个右值引用:Foo(Foo&&)
。如果是从一个临时变量来构造新的Foo
的话,编译器会优先调用搬移构造函数,来把临时对象开膛破肚,取出自己需要的东西。拷贝构造函数就不敢这么做,因为它不确定对象在其他地方有没有被使用,如果误操作了,恐怕会被人打。
std::move
C++11提供了std::move
来废弃一个对象,也就是把它标识为临时变量,这样可以丢给搬移构造函数处理,比如下面的例子:
std::string x1 = "hello, world!";
std::string x2 = std::move(x1);
std::cout << "x1 = " << x1 << std::endl;
std::cout << "x2 = " << x2 << std::endl;
由于x1
通过std::move
被标识为临时对象,于是就被x2
给开膛破肚,存储的内容被抢走了。上面的例子输出结果是:
x1 =
x2 = hello, world!
C++11之后标准库中大部分容器都支持搬移构造,都可以被开膛破肚。
std::arrary
是少数的例外。
所以,C++11给了我们一个选择,对于函数的返回值,如果编译器没有办法自动优化,我们提供搬移构造函数给编译器,从而降低拷贝构造的成本。
小结
作为小结的话,我决定从How to use move semantics with std::string during function return? duplicate
摘抄CodeAngry的Comments:
1. Return by const reference& if the A object will outlive the reference's scope and you need it readonly. 2. If the A object gets out of scope and/or you need to copy/modify it, return a value. 3. If you need to modify the original value return, while it stays in scope, reference&. --- Don't return reference then make a copy to modify it, always return value for write access. – CodeAngry Jul 14 '15 at 12:35
其他参考链
- Is returning by rvalue reference more efficient?这篇文章说了为什么返回右值引用是一个错误的行为,以及怎么做是正确的。
- Does a const reference prolong the life of a temporary?
- C++ Returning reference to local variable
- Return a local object rvalue reference,right or wrong?
- How to use move semantics with std::string during function return?
duplicate
- C++11 rvalues and move semantics confusion (return statement)
- C++ Rvalue References Explained
- Rvalue references in Chromium
- The new C++ 11 rvalue reference && and why you should start using it
- Can someone explain rvalue references with respect to exceptions?
- Is std::array movable?
- Does a const reference prolong the life of a temporary
- Universal References in C++11—Scott Meyers
- Are the days of passing const std::string & as a parameter over?
- C++ Returning reference to local variable