C++ 的左右值与左右值引用

in #cn-programming6 years ago

左值与右值

C++ 中左值和右值的概念来源于 C, 在 C 中左值和右值的区别很简单, 能出现在赋值号左侧的就是左值, 否则就是右值. 比如变量是左值, 字面常量或者 const 定义的常量是右值.

然而在 C++ 中, 左值和右值的区别就不再是那么简单了, 甚至和 C 还会有冲突, 比如在 C 中 const 定义的常量对象是右值, 而在 C++ 中却是左值.

实际上在 C++ 中左值和右值的情况非常复杂, 有时区分他们也是非常困难的. Scott Meyers 大师在其 Effective Modern C++ 一书所说的不失为一个好方法, 在理解这句话之前, 我们一定要有一个意识, 就是左值和右值是表达式的属性, 代表着表达式的运算结果是左值还是右值.

A useful heuristic to determine whether an expression is an lvalue is to ask if you can take its address. If you can, it typically is. If you can’t, it’s usually an rvalue. A nice feature of this heuristic is that it helps you remember that the type of an expression is independent of whether the expression is an lvalue or an rvalue.

也就是说, 表达式是左值还是右值, 就看这个表达式的结果是否在内存有对应的存储区域, 有的话就是左右, 没有的话就是右值 (TODO: 补充例子), 这点也可以参考 c++ primer 5th, p121.

另外还有两个有重要的准则:

  1. 对象 (对象是最简单的表达式) 被用作左值时, 用的是对象的地址; 被用作右值时, 用的是对象的值
  2. 在需要右值的地方, 可以用左值代替 (因为地址中存储着值), 这时左值被当成右值使用, 用的是左值里存储的值. 这条准则只有一个例外, 就是左值引用不能当做右值引用使用 (下面会讲到引用)

引用

在 C++11 以前, 引用就是对象的别名, 只能绑定到左值上, 并且一旦绑定到了某个对象上就永远绑定了, 不能通过赋值改变引用绑定的对象, 因此在定义引用时必须初始化.

定义引用绑定到对象时, 一般是要求所绑定到的对象的类型和引用的类型必须一致, 不过也有两个例外:

  1. 定义的基类引用可以用派生类对象初始化 (c++ primer 5th, p534)
  2. 常量引用 (对 const 的引用) 可以用任意表达式作为其初始值, 只要该表达式的结果能转换成引用类型即可 (c++ primer 5th, p55)

上面说的引用只能绑定到左值上, 在 C++11 中扩展了引用的概念, 允许引用绑定到右值表达式这样的右值上, 这新引入的引用类型被称为右值引用, 对应的之前的引用被称为左值引用.

那么右值引用有什么用呢? 如果仅仅是让我们能够绑定到字面常量这样的右值的话, 刚刚我们也看到了, 通过常量(左值)引用我们同样也能够绑定到字面常量等右值表达式上, 为什么还要引入右值引用呢?

然而右值引用不仅仅是让我们能够不借助 const 就能引用右值表达式, 它还支持了对象移动以及完美转发这两个重要的能力. 关于这两个能力就是另外的话题了, 我们简单介绍一下对象移动.

对象移动

对象移动的目的是减少对象拷贝, 被移动的对象内容会被 "掏空", 赋予新对象中, 这个操作依赖于 "移动构造函数" 或者 "移动操作符", 然而如何定义这两个方法却成了一个问题, 我们知道拷贝构造函数和赋值操作符是以对象的引用 (左值引用) 为参数的, 而如果要定义移动构造函数或移动操作符, 其参数当然也得以代表对象的某种形态为参数, 对象的引用这种形态已经不能用了, 对象的非引用更不能用 (原因见 c++ primer 5th, p422), 那么只能新发明一中语义了, 这个新的语义就是右值引用, 写作 T &&. 于是移动构造函数就可以这么定义了:

Foo (Foo && other);

如此一来, 只要传递给构造函数的是右值引用, 就会调用移动构造函数, 免去拷贝所造成的开销.

然而如何传递右值引用呢? 当发生如下情况时, 该调哪个构造函数又成了一个问题:

Foo (const Foo &other);
Foo (Foo && other);

Foo(foo);    // 该调哪个构造函数?

事实上由于拷贝构造函数先出现, 所以上面第 4 行的写法当然是会调拷贝构造函数的. 为了能够调到移动构造函数, 标准库提供了一个 std::move 方法, 这个方法总是返回右值引用, 因此我们只要这么写就能够调用移动构造函数了:

Foo(std::move(foo));

除了显式的通过 std::move 方法, 有些情况下编译器也能够自动判断是否可以调移动构造函数来降低开销, 比如编译器发现源对象是个临时对象.

关于右值引用带来的另一个功能完美转发就不再敖述, 可以参考文末链接 1 关于右值引用的提案.

参考

  1. 右值引用提案: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1690.html

Originally posted on
http://cifer.me/2018/04/30/c-%E7%9A%84%E5%B7%A6%E5%8F%B3%E5%80%BC%E4%B8%8E%E5%B7%A6%E5%8F%B3%E5%80%BC%E5%BC%95%E7%94%A8/

Sort:  
Loading...

Coin Marketplace

STEEM 0.31
TRX 0.11
JST 0.034
BTC 64332.82
ETH 3146.25
USDT 1.00
SBD 4.17