静心学习之路(9)——左值和右值

重载符号的返回推断

我们先来一个简单的东西,问下面这个是个什么玩意?

int i = 32;

你翻了个白眼,说你当我是沙口吗,这不就是个整型定义吗。当然你说的没错,包括入门课的老师也会这么说,不过这也可以看成是一个函数,是调用了class intoperator=(int&& rhs)这个函数,符号左边是class,右边的是参数,既然是个函数,那么函数的返回值去哪里了呢?

具体看下几个常见表达式,观察下整个表达式的返回值:

void main()
{
    int i = 0, j = 0, k = 0;
    printf("%s:%d:%d\n", __FUNCTION__, __LINE__, (i = i + 1));    // 1
    printf("%s:%d:%d\n", __FUNCTION__, __LINE__, (j += 1));       // 1
    printf("%s:%d:%d\n", __FUNCTION__, __LINE__, (++k));          // 1
    printf("%s:%d:%d\n", __FUNCTION__, __LINE__, (k++));          // 1

    system("pause"); 
}

不绕圈子,上面四个结果打印的都是1。对于前两个操作,说明函数int& operator=(int)int& operator+=(int)两个返回的都是返回引用(*this),用下面这个例子可以更容易理解,即 = 操作返回的是左值。

void main()
{
    int a = 0, b = 1, c = 2;
    a = b = c;  // a = (b = c);
    printf("%s:%d:%d%d%d\n", __FUNCTION__, __LINE__, a, b, c);  // 2 2 2

    a = 0, b = 1, c = 2;
    (a = b) = c;
    printf("%s:%d:%d%d%d\n", __FUNCTION__, __LINE__, a, b, c);  // 2 1 2

    system("pause"); 
}

上面(包括a = (b = c);的情况)输出了222,说明最后执行了a = b,下面输出了212,说明最后执行了 a = c。同时也知道了 = 操作的优先级是从后往前的。

operator++的大概和下面的实现方式类似(随便搜的源码:rapidxml),++i 返回自己的引用(左值),i++ 返回值传递(右值),相当于变化前的副本。

node_iterator& operator++()
{
    assert(m_node);
    m_node = m_node->next_sibling();
    return *this;
}

node_iterator operator++(int)
{
    node_iterator tmp = *this;
    ++this;
    return tmp;
}

我们可以知道右值是不能被修改的,因为值还没有被绑定到一个引用上(左值)。

int p = 0;
(++p)++;
++(++p);
(p++)++;  // error
++(p++);  // error
(p = p + 1)++;
(p += 1)++;

++i 操作是返回引用的,所以可以嵌套调用。了解返回的是引用还是值传递以后,你就可以弄懂以前老师发的各种++八股文题目了。

(大学老师对operator++的解释很多都是扯淡的,有的还说是i++是「先执行这行代码的其他部分,最后执行+1操作」,我到现在都搞不懂为什么会这样解释给学生听 - -)

右值引用

我们再来重新思考下 & *操作符的意义,我们先做一些简单的引用:

void main()
{
    int i = 32;
    int& r = i;
    int* p = &r;
    int* q = &i;
    printf("%s:%d:%d %d %d %d %d %d\n", __FUNCTION__, __LINE__, i, r, *(p), *(q), p, q);
    // 32 32 32 32 2553604 2553604

    system("pause"); 
}

我们其实可以发现p和q的地址是一样的,说明了其实i和r,p和q都是一样的东西。然后我们可以知道通过i= r= (*p)= (*q)= 都可以改变彼此的值,说明了这些操作符都属于左值引用

当一个表达式本身返回的是右值而不是左值时,我们就可以用右值引用&&,比如乘法操作符int operator*(int)返回的肯定就只是一个临时的结果,同样的,字面常量也是一个右值,而一个变量本身返回的就是它的引用,属于左值。

i * 32;  // rvalue
int&& rr1 = i * 32;
int& r = i * 32;  // error
//const int& r = i * 32; 

32;  // rvalue
int&& rr2 = 32;
int& r = 32;  // error
//const int& r = 32; 

i;  // lvalue
int&& rr = i; // error
int& r1 = i; 

rr2;  // lvalue, not rvalue (val)
int&& rr = rr2;  // error
int& r2 = rr;

(以上 int& r //error 的情况前面加个const 是可以通过的)有人说我想让*操作返回引用行不行,可以啊,重载下operator*就可以了。不过这属于反直觉的行为,重载符号要尽量信达雅,把自己搞晕就不好了。

那么知道了右值引用只能绑定临时变量,那到底有啥用呢,和我左值引用的方式定义一个临时变量有啥区别呢?按照《c++ primer》的说法,左值持久,右值短暂,「使用右值引用的代码可以自由接管所引用对象的资源」。大概意思就是右值引用肯定会被销毁,所以可以乱搞?

我们不能通过右值引用来赋值int&& rr = i,如果我们希望用右值的方法处理一个左值,标准库有一个std::move的函数可以返回对象的右值引用。用法大概如下:

int &&rr = std::move(i);
// 此时相当于执行了 int &rr = i;,不过rr指向的是即将被销毁的对象

这样的话这个i就不再能够正常使用,调用std::move之后i能够进行的操作就只有赋值和销毁。

移动构造函数和移动赋值运算符

我们先把string这类放一边,对于一般的结构体Foo的operator() operator=操作,一般又怎么思考呢?一般我们的直觉理解就是,左值参数就是拷贝,不能影响引用,右值参数就直接移动,然后废弃就行了。

class Foo {
public:
  Foo() = default;                      // 默认构造函数
  ~Foo() = default;                      // 析构函数
  Foo(Foo&) = delete;                // 拷贝构造函数
  Foo& operator=(Foo& rhs) = delete;  //拷贝赋值运算符

  Foo(Foo&& s);                        // 移动构造函数
  Foo& operator=(Foo&& rhs);  //移动赋值运算符
};


Foo::Foo(Foo&& s) noexcept
: element(s.element)
{
  s.element = nullptr;
}

Foo& Foo::operator=(Foo&& rhs) noexcept
{
  if (this != &rhs)  // check =self
  {
    free();
    elements = &rhs.element;
    rhs.elements = nullptr;
  }
  return *this;
}

这里注意到参数是确定了一个右值引用,这样可以保证操作的一定是一个临时变量,不会影响到其他的地方。但是下面有个地方貌似多余的检查了一下是否自我赋值,因为如果指针相同的话两边指向的是同一块地方,此时无需操作,反而析构了自我资源会引发错误,因为这个右值可能是std::move调用自己的结果。

最佳实践里说明了std::move是一个相当危险的函数,必须要绝对确认源对象没有其他用户,且移动操作是确信必要的且保证安全的情况下,才可以使用std::move

提到std::move函数就不得不提下std::forwardstd::forward<T>(arg)可以把arg从左值和右值引用推断转换,中文翻译叫完美转发,有点浮夸,就是为了保持给定实参的左值/右值属性。

template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 && t2)
{
  f(std::forward<T2>(t2), std::forward<T1>(t1));
}

这样调用flip(g, i, 42)i将以int&的类型、42将以int&&的类型,传值给g

引用

《C++ Primer 5》13.6.对象移动 p470

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

推荐阅读更多精彩内容