重载符号的返回推断
我们先来一个简单的东西,问下面这个是个什么玩意?
int i = 32;
你翻了个白眼,说你当我是沙口吗,这不就是个整型定义吗。当然你说的没错,包括入门课的老师也会这么说,不过这也可以看成是一个函数,是调用了class int
的operator=(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::forward
,std::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