从一个需求出发理解rvalue

需求

一年前在游戏公司做游戏开发,最近突然回想起当时在公司看到的一段代码,其使用方式如下

1
2
3
4
5
Package package;
Object* o = new Object;
package.objects.push_back(o);

package.forEach(setId(1) + setName("adf"));

作用是对包裹中的每一个道具设置id为1, 名字为adf

分析

重点在于package.forEach(setId(1) + setName("adf"));这一行

如果简单想一下,实现上也无非就是一个基类Action存在一个类似于doAction的方法,即

1
2
3
class Action {
virtual void doAction(Object* o) = 0;
}

然后子类再继承自Action,实现以下doAction方法,最后在配合重载+号运算符,返回一个class ActionSet : public Action
再再ActionSet中通过for循环调用加号两边的Action即可, 简单如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ActionA : public Action {
virtual void doAction(Object* o) {
printf("in ActionA::doAction");
}
};

class ActionB : public Action {
virtual void doAction(Object* o) {
printf("in ActionB::doAction");
}
}

class ActionSet : public Action {
std::vector<Action*> actions;
virtual void doAction(Object* o) {
for each action in actions do action->doAction(o);
}
}

到这里,目前来说没什么疑问,但是其中有坑,后面讲, 接下来看+号运算符重载

重载+号运算符

由于学艺不精,第一个版本如下

1
2
3
4
5
6
ActionSet operator + (Action* a, Action* b) {
ActionSet res;
res.actions.push_back(a);
res.actions.push_back(b);
return res;
}

但是编译直接报错,因为+号运算符的参数不支持指针, 原因可自行搜索stackoverflow. 所以修改为如下方式

1
2
3
4
5
6
ActionSet operator + (Action a, Action b) {
ActionSet res;
res.actions.push_back(&a);
res.actions.push_back(&b);
return res;
}

但是,又一个问题, 这里传入的是值对象,先不考虑Action存在纯虚函数会导致这里编译失败, 仅仅是Action存在继承体系,
若参数为Action的子类, 则会因为这里为值对象而导致最终调用的为父类的函数而非子类,所以并不能达到目的,于是修改为如下

1
2
3
4
5
6
ActionSet operator + (Action& a, Action& b) {
ActionSet res;
res.actions.push_back(&a);
res.actions.push_back(&b);
return res;
}

这里使用了引用,所以在调用时就不会出现上面所说调用函数为父类而非子类的问题,目前看起来一切完美。

实现更优雅的调用

就上面的代码,若要使用,还是满费劲的,需要类似如下步骤

1
2
3
ActionA a;
ActionB b;
ActionSet ab = a + b;

那么如何实现像setId(1) + setName("adf")一样简单的调用呢?

通过增加两个辅助函数, 使其返回值为ActionA和ActionB对象, 即如下

1
2
3
4
5
6
7
8
9
10
11
ActionA setA(int val) {
ActionA res;
set val to res;
return res;
}

ActionB setB(int val) {
ActionB res;
set val to res;
return res;
}

可以看到,setAsetB结构极为相似, 于是使用宏定义,以使今后的扩展更为方便,如下

1
2
3
4
5
6
7
8
9
#define SetPropFunc(_func, _type, _prop, _act_type) \
_act_type set##_func(_type _prop) { \
_act_type __act; \
__act._prop = _prop; \
return __act; \
}


SetPropFunc(Id, int, id, SetIdAction);
SetPropFunc(Name, std::string, name, SetNameAction);

于是调用就变成了setA(val) + setB(val), 写好后开心的拿去编译。。

开始噩梦般的错误

怎么一堆报错, 其中有如下错误

1
invalid operands to binary expression ('ActionA' and 'ActionB')

ActionA, ActionB不是Action的子类么, 怎么这里却说非法的操作数呢?

一番google之后,发现了这东西和右值(rvalue)有关系,其中有一点很重要的是

右值不能被reference引用,可被const reference引用

什么意思?需要加个const?先试试吧,于是再次修改+号运算符的重载如下

1
2
3
4
5
6
ActionSet operator + (const Action& a, const Action& b) {
ActionSet as;
as.actions.push_back(&a);
as.actions.push_back(&b);
return as;
}

然后发现这一处错误消除, why?

先说说右值, setAsetB 两个函数返回的对象都为右值, 怎么确定的?
试用如下代码

1
&(setA(val))

发现会报错taking the address of a temporary object of type 'ActionA', 即不能取临时对象的地址

而有一个判断是左值还是右值的快速方法
如果能取到地址,则为左值,如果不能取到地址则为右值

所以我们在调用operator+时,传入的两个参数都是右值,而右值又只能被const reference引用,所以加上
const就正确了

接下来先引入Package的初始版实现

1
2
3
4
5
6
7
8
9
class Package {
public:
std::vector<Object*> objects;

void forEach(Action& act) {
for (auto o : objects) {
act.doAction(o);
}
};

那么也就有了调用package.forEach(setA(val) + setB(val))

但是:还是有错,non-const lvalue reference to type 'Action' cannot bind to a temporary of type 'ActionSet'
这里意思就很明确了,非const的左值引用不能绑定到临时对象, 和上边右值那个时一个问题, 加个const就好了
于是有

1
2
3
4
5
6
7
8
9
class Package {
public:
std::vector<Object*> objects;

void forEach(const Action& act) {
for (auto o : objects) {
act.doAction(o);
}
};

然后,消除一个错误,还是有一堆错误,原因在于forEach 这里的act已经被const修饰,也就是说不能调用其
非const函数, 所以对上边所有Action的定义中的doAction都需要加上const修饰,表示这个函数不会修改成员变量

最终版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class Object {
public:
int id;
std::string name;
};

class Action {
public:
virtual void doAction(Object* o) const {
printf("in Action::doAction\n");
}
};

class SetIdAction : public Action {
public:
int id;
virtual void doAction(Object* o) const {
printf("in SetIdAction::doAction\n");
o->id = this->id;
}
};

class SetNameAction : public Action {
public:
std::string name;
virtual void doAction(Object* o) const {
printf("in SetNameAction::doAction\n");
o->name = this->name;
}
};

class ActionSet : public Action {
public:
std::vector<const Action*> actions;

virtual void doAction(Object* o) const {
printf("in ActionSet::doAction\n");
for (auto action : actions) {
action->doAction(o);
}
}
};

class Package {
public:
std::vector<Object*> objects;

void forEach(const Action& act) {
for (auto o : objects) {
act.doAction(o);
}
}
};

ActionSet operator + (const Action& a, const Action& b) {
ActionSet as;
as.actions.push_back(&a);
as.actions.push_back(&b);
return as;
}

#define SetPropFunc(_func, _type, _prop, _act_type) \
_act_type set##_func(_type _prop) { \
_act_type __act; \
__act._prop = _prop; \
return __act; \
}


SetPropFunc(Id, int, id, SetIdAction);
SetPropFunc(Name, std::string, name, SetNameAction);

int main(int argc, char *argv[])
{

Package package;
Object* o = new Object;
package.objects.push_back(o);

package.forEach(setId(1) + setName("adf"));

printf("%d %s\n", o->id, o->name.c_str());

return 0;
}

和最开始的相比,主要是在适当的位置增加了很多的const。总结如下:

  • 右值不能被reference引用,可被const reference引用
  • 如果能取到地址,则为左值,如果不能取到地址则为右值
  • 对需不修改成员变量的函数,尽可能的函数定义末尾用const修饰
  • 对于参数,如果可以用const修饰,尽量用const
文章目录
  1. 1. 需求
  2. 2. 分析
  3. 3. 重载+号运算符
  4. 4. 实现更优雅的调用
  5. 5. 开始噩梦般的错误
  6. 6. 最终版本
,