左值(lvalue),右值(rvalue) 是 c/c++ 中一个比较晦涩的概念,有的人可能甚至没有听过,但这个概念到了 c++11 后却变得十分重要,它们是理解move(),forward()等新语义的基础。
左值与右值这两概念是从c中传承而来的,在c中,左值指的是能够出现在等号左边及右边的变量(表达式),右值则指的是只能出现在等号右边的变量(表达式).
int a;
int b;
a = 3;
b = 4;
a = b;
b = a;
//不合法。
3 = a;
a+b = 4;
在 c 语言中,通常来说有名字的变量就是左值(如上面例子中的a, b),而由运算操作(加减乘除,函数调用返回值等)产生的中间结果(没有名字)就是右值,如上的3+4, a + b等。
我们暂且可以认为:左值就是在程序中能够寻值的东西,右值就是没法取到它的地址的东西(不完全准确),但如上概念到了c++中,就变得稍有不同。
在c++中,每一个表达式都会产生一个左值,或者右值,相应的,该表达式也就被称作“左值表达式”,“右值表达式”。对于内置的基本数据类型来说(build-in types),左值右值的概念和 c 没有太多不同,不同的地方在于自定义的类型。
而且这种不同比较容易让人混淆:
1) 对于内置的类型,右值是不可被修改的(non-modifiable),也不可被const, volatile 所修饰(cv-qualitification ignored)
2) 对于自定义的类型(user-defined types), 右值却允许通过它的成员函数进行修改。
对于1),这和c是一致的,2) 却是c++中所独有, 因此,如果你看到c++中如下的写法,千万不要惊讶:
class cs
{
public:
cs(int i): i_(i) { cout << "cs(" << i <<") constructor!" << endl; }
~cs() { cout << "cs destructor,i(" << i_ << ")" << endl; }
cs& operator=(const cs& other)
{
i_ = other.i_;
cout << "cs operator=()" << endl;
return *this;
}
int get_i() const { return i_; }
void change(int i) { i_ = i; }
private:
int i_;
};
cs get_cs()
{
static int i = 0;
return cs(i++);
}
int main()
{
// 合法
(get_cs() = cs(2)).change(323);
get_cs() = cs(2);// operator=()
get_cs().change(32);
return 0;
}
这个特性多少有些奇怪,通常来说,c++ 中的自定义类型是应该设计地尽量和内置类型一样才对的,但这个特性却偏偏违背了这个原则。
对于这个特性,我们其实可以这样想,也许会好理解点:自定义类型允许有成员函数,而通过右值调用成员函数是被允许的,但成员函数有可能不是 const 类型,因此通过调用右值的成员函数,也就可能会修改了该右值,done!
关于右值,有一个十分值得关注的语言的特性:右值能被 const 类型的引用所指向
const cs& ref = get_cs();
而且只能被const 类型的 reference 所指向:
//error
cs& ref = get_cs();
当一个右值被 const reference 指向时,它的生命周期就被延长了,这个用法我在前面一篇博客里讲到过它的相关应用,点这。
这里暗藏逻辑其实就是:右值不能直接转化成左值(但左值可以转化为右值).
关于前面提到的右值的两个特性:
1) 允许调用成员函数。
2) 只能被const reference指向。
导致了一些比较有意思的结果,比如:
void func(cs& c)
{
cout << "c:" << c.get_i() << endl;
}
//error
func(get_cs());
//正确
func(get_cs() = get_cs());
其中:func(get_cs() = get_cs()); 能够被正常编译执行的原因就在于,cs 的成员函数 operator=() 返回的是 cs&!
不允许非 const reference 引用 rvalue 并不是完美的,它事实上也引起了一些问题,比如说拷贝构造函数的接口不一致了,这是什么意思呢?
class cs
{
public:
cs& operator=(const cs& c);
};
// 另一种写法
class cs2
{
public:
cs2& operator=(cs2& c);
};
上面两种写法的不同之处就在于参数,一个是const reference,一个是非const.
通常来说,如果不需要修改传进来的参数,我们往往就按 const reference 的写法,但对于copy constructor 来说,它经常是需要修改参数的值,比如 auto_ptr。
// 类似auto_ptr
class auto_ptr
{
public:
auto_ptr(auto_tr& p)
{
ptr_ = p.ptr_;
p.ptr_ = NULL;
}
private:
void* ptr_;
};
所以,对于auto_ptr来说,它的 copy constructor 的参数类型是 non const reference。
这个种写法本来应该被鼓励的,non const reference 比 const reference 更能灵活应对各种情况,从而保持一致的接口类型。
但如果拷贝构造函数写成这样子,却又对 rvalue 的使用带来了极大的不变,如前面所讲的例子,rvalue 不能被 non const reference 所引用,所以像auto_ptr的这样的 copy constructor 就不能接受 rvalue.
//错误
auto_ptr p(get_ptr());
// operator=() 同理,错误。
auto_ptr p = get_ptr();
这也是auto_ptr很不好用的其中一个原因。
为了解决这个问题,c++11 中引入了一种新的引用类型,该种引用是专门用来指向 rvalue 的,有了这种新类型,对 lvalue 和 rvalue 的引用就能明确区分开来了,而在之前,它们是一样的。
因为有了这种新的类型,接着就引出了c++11 中新的语义,move(), forward() 等,这儿先卖个关子,我们下次再讲。