右值引用小结

今天总结下右值的那些事儿

  • 什么是右值
  • 右值的必要性
  • move函数

什么是右值

传统c++的引用就是左值引用,使得标识符关联到左值。左值是一个表示数据的表达式,如变量名或指针等,程序可以获取其地址。

右值引用使用 && 来表示,右值引用可关联到右值,即可出现在赋值表达式的右边,但不能对其应用地址运算符的值。

左值其实就是可以通过内存地址访问的值,而右值则相反,是不能通过地址访问的值

说了这么多貌似还是不知道右值是什么,右值一边有这三种类型:

  • 字面常量
  • 诸如 x+y 等表达式
  • 返回值的函数(该函数返回的不是引用,也不是指针,直接返回对象那种)

值分左右

C++ 里有左值和右值。这话不完全对。标准里的定义实际更复杂,规定了下面这些值类别(value categories):

  • lvalue:通常是可以放在左边的表达式,左值
  • rvalue:通常是只能放到右边的表达式,右值
  • glvalue:generalized lvalue,广义左值
  • xvalue:expiring value,将亡值
  • prvalue:纯右值

我们先看lvalue和prvalue。
左值lvalue,是有标识符,可以取地址的表达式

  • 变量、函数或数据成员的名字
  • 返回左值引用的表达式,如 ++x、x = 1、cout << ' '
  • 字符串字面量如 "hello world"

在函数调用的时候,左值可以绑定到左值引用的参数,如T&,而常量只能绑定到常左值引用 const T&

反之,右值就是没有标识符,不可以取地址的表达式

  • 返回非引用类型的表达式,如 x++、x + 1、make_shared(42)
  • 除字符串字面量之外的字面量,如 42、true

注意,返回为非引用类型的表达式就是右值,而引用类型只有两种类型,指针和引用,其它都是非引用类型。make_shared返回的是一个智能指针,其实也是一个智能指针的对象,不是引用型,当然也是右值。还有一点需要注意的是,对于指针通常采用值传递,所以我们并不关心是左值还是右值

smart_ptr ptr2 = std::move(ptr1);

std::move(ptr),它的作用就是把一个左值强制转换成一个右值引用,但并不改变它的内容。我们可以把std::move(ptr)看成一个有名字的右值,c++里,这种就属于xvalue,将亡值

右值的必要性

假设现在有如下代码:

函数 allcaps 并没有返回引用或指针,直接在函数体内构造了一个临时变量并且返回,临时变量是会被删除的。但vstr_copy2依然会调用复制构造函数来构造一个新的对象,如此,上述过程中,先是构造了一个临时变量 temp,再调用复制构造函数生成了一个新对象 vstr_copy2,再删除了temp,temp的生命周期非常非常短,没有什么作用,中间还产生了大量的内存操作。是否可以避免多作的内存操作呢?

是的,可以的,右值引用的作用就体现了。右值引用搭配移动构造函数就能起到这样的效果。让我们来看一个例子:

class Useless
{
public:
Useless();
~Useless();
explicit Useless(int k);
Useless(int k, char ch);
Useless(const Useless& f);
Useless(Useless&& f);
Useless operator+(const Useless& f) const;
void showData() const;

private:
int n;
char* pc;
static int ct;
void showObject();
};


int Useless::ct = 0;

Useless::Useless()
{
++ct;
n = 0;
pc = nullptr;
cout << "default constructor called, number of objects : " << ct << endl;
}

Useless::Useless(int k) : n(k) {
++ct;
cout << "int constructor called, number of objects : " << ct << endl;
pc = new char[n];
showObject();
}

Useless::Useless(int k, char ch) : n(k) {
++ct;
cout << "int char constructor called, number of objects : " << ct << endl;
pc = new char[n];
for (int i = 0; i < n; i++)
{
    pc[i] = ch;
}
showObject();
}

Useless::Useless(const Useless& f) :n(f.n) {
++ct;
cout << "copy constructor called, number of objects : " << ct << endl;
pc = new char[n];
for (int i = 0; i < n; i++)
{
    pc[i] = f.pc[i];
}
showObject();
}

Useless::Useless(Useless&& f) : n(f.n) {
ct++;
cout << "move constructor called, number of objects : " << ct << endl;
pc = f.pc;
f.pc = nullptr;
f.n = 0;
showObject();
}

Useless::~Useless()
{
cout << "destroy called, object left" << --ct << endl;
cout << "delete object: \n";
showObject();
delete[] pc;
}

Useless Useless::operator+(const Useless& f) const {
cout << "enter operator +" << endl;
Useless temp = Useless(n + f.n);
for (int i = 0; i < n; i++)
{
    temp.pc[i] = pc[i];
}
for (int i = n; i < temp.n; i++)
{
    temp.pc[i] = f.pc[i - n];
}
cout << "temp object : \n";
cout << "leaving operator +" << endl;
return temp;
}

void Useless::showObject() {
cout << "num of elements" << n;
cout << "data address " << (void*)pc << endl;
}

void Useless::showData() const{
if (n == 0) {
    cout << "empty object";
}else{
    for (int i = 0; i < n; i++)
    {
        cout << pc[i];
    }
    cout << endl;
}
}

如上所示,这么来调用:

  Useless four(one + three);

one+three,第一小节中有提到过,这是一个右值,然后会再调用

Useless(Useless&& f);

这种函数称为移动构造函数,它的特殊之处在于,不会重新地进行内存操作,为 pc 指针进行内存初始化,而是直接将 右值引用 的pc内存地址赋值给自己,这样就省去了一次内存操作了,然后将右值的pc指针指向空指针,这是因为,一个内存如果有两个指针指向它,如果另一个对象被析构了,pc被删除了,另一个pc指针就会成为野指针,不好跟踪,所以一般地移动构造函数都会将右值指针清空。

生命周期

一个变量的生命周期在超出作用域的时候就会结束,那么纯右值呢?

C++ 的规则是:一个临时对象会在包含这个临时对象的完整表达式估值完成后、按生成顺序的逆序被销毁,除非有生命周期延长发生。

class shape {
public:
virtual ~shape(){};
};
class circle: public shape {
public:
circle(){
    cout << "create circle" << endl;
}
~circle(){
    cout << "delete circle" << endl;
}
};

class triangle: public shape {
public:
triangle(){
    cout << "create triangle" << endl;
}
~triangle(){
    cout << "delete triangle" << endl;
}
};

class result{
public:
result(){
    cout << "create result" << endl;
}
~result(){
    cout << "delete result" << endl;
}
};

result process_result(const shape& shape1, const shape& shape2){
return result();
}

void test_rvalue(){
cout << "test_rvalue" << endl;
process_result(circle(),  triangle());
cout << "end" << endl;
}

上述示例程序中,process_result函数中传入的两个参数是临时对象(纯右值),因为它们返回的并不是引用(circle和triangle的构造函数返回的并不是引用类型),这些临时对象的生命周期只在自己的那一行,process_result函数调用完它们的生命周期就结束了。如果我们改一下,变成

result&& rvalue = process_result(circle(), triangle());
//日志:
test_rvalue
create circle
create triangle
create result
delete triangle
delete circle
end
delete result

从日志上看,result对象的生命周期被延长到了整个大括号内

如果一个 prvalue 被绑定到一个引用上,它的生命周期则会延长到跟这个引用变量一样长。

注意,生命周期延长,只对prvalue有效,将亡值无效

引用折叠

左值引用和右值引用作为参数表示方式分别为:

template <class T>
void f(T&);
template <class T>
void f(T&&);

在调用函数f的时候,我们可能传入一个左值,也可能传入一个右值。

由于存在T&&这种未定的引用类型,当它作为参数时,有可能被一个左值引用或者右值引用的参数初始化,这时候经过类型推导的T&&类型相比右值引用会发生变化。

具体规则是:
1.所有右值引用折叠到右值引用上仍然是一个右值引用。(A&& && 变成 A&&)
2.所有的其他引用类型之间的折叠都将变成左值引用。 (A& & 变成 A&; A& && 变成 A&; A&& & 变成 A&)

move函数
导入 intility 头文件,即可以使用move函数,move函数在 std 的命名空间当中。它的作用就是,把一个左值强制转换为右值。如果构造函数的实参在使用完之后就可以丢弃,那就可以把此左值强制转换化右值,使用移动构造函数,这样就会节省一些内存操作。

注意,move完之后的参数,只能丢弃或者重新赋值,不能再使用了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,640评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,254评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,011评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,755评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,774评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,610评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,352评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,257评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,717评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,894评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,021评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,735评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,354评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,936评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,054评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,224评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,974评论 2 355

推荐阅读更多精彩内容