C++注解

右值引用与移动语义

C++11提供的新特性,相当有用,节省了很多开销,生了很多事。(ps:越来越感觉C++ 11的很多新特性有意在淡化raw指针的影响,有点java)

首先来大概了解一下什么事右值

右值

右值,通常人们说放在等号 右边的值就是右值。

但是这只片面的,并不全面。

我们来看看传统的左值引用

1
2
3
4
5
6
7
// 传统的左值引用
int a = 10;
int b = 20;
int c = a + b;
int& la = a;
int& lb = b;
int& lc = c;

a 就是左值,一个符号,有地址(在栈上),而10 本身就是一个右值。没有办法&取地址,一个常量。

a+b 实际上也是一个常量公式,也没办法取地址,也是只能放右边。

具体详细可以看cppreference对值类别的分类。

而我们使用的右值引用后

1
2
3
4
5
6
7
int&& ra = 10;
int&& rc = a + b; // 右值引用
cout << ra << char(10);
cout << rc << char(10);
// 在右值引用后,原本的右值的就有了地址
cout << &ra << char(10);
cout << &rc << char(10);

可以打出右值引用的地址位置了。

问题引出

右值引用的提出,减少了很多不必要的内存开销,假设:当我们传递一个右值时,右值是一个比较大的自定义类(或者字符串),当我们使用值传递,或者拷贝构造时,会同样的构造一个同样临时变量,再传递给一个左值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//这里拿string 假设无移动构造,但是有拷贝构造
string func(const string temp){
string tempret = temp;
return tempret;
}

int main(){
string a;
//假设a对象管理着有2000000个字符
string b(temp);
string c(func(b));
string d = func(c);
}

如果使用拷贝构造 或者赋值拷贝,单是临时对象的new和delete的内存操作就会执行好多次,而且调用一次200000个字符。这个内存IO 开销,是很耗资源的,并且很多都是无用功。

C++ 是个追求高效,性能的语言,就引入了右值引入这个概念,进而引入了移动构造。

不过,其实编译器本身也很聪明,也会消除 无用功的的额外工作,消除构建临时参数对象(copy elision),不过,通过使用右值引用,用户可以自己指出何时使用移动语义,而不是通过编译器来优化。

移动构造

对比一下常见的拷贝构造

1
2
3
4
Myclass (const Myclass & f){
//m_ptr 为成员变量 Myclass*;
m_ptr = new Myclass();
}

移动构造

1
2
3
4
5
Myclass ( Myclass && f){
//m_ptr 为成员变量 Myclass*
m_ptr = f.m_ptr;
f.m_ptr = nullptr;
}

首先,是函数参数的不同,可以发现,参数接受的是一个右值

其二,不再需要进行内存的申请,而是直接使用右值拥有的内存,然后将原有的成员变量的指针赋值为空值的操作,所以也没有const修饰符。

移动赋值

同理, 赋值构造,当然也就有移动赋值。

1
2
3
4
5
6
7
8
9
10
Myclass&  operator=( Myclass && f){
//m_ptr 为成员变量 Myclass*;
if(this == &f){
return *this;
}
delete m_ptr;
m_ptr = f.m_ptr;
f.m_ptr = nullptr;
return *this;
}

首先,先检查自己是否是自己给自己赋值,如果是,直接返回自己本身。

首先就是释放原有的指针,避免出现悬挂指针,然后就是简单的赋值操作。

当然了,也不能是const,因为改变了引用对象

使用移动语义

当我们想让自己的类对象构造,或者赋值,等操作,一般直觉的都会使用左值传入使用,编译器也就会默认调用拷贝构造等一般的左值引用函数。

当我们就想指定传入的左值以右值的方式调用右值引用函数,

我们可以使用运算符静态转换static_cast<>,将传入对象的类型强制转换为类型&& 右值。

不过,C++ 11提供了一个强制移动的转换函数 std::move。也可以达到更简单的效果。(函数实现也是调用的static_cast<>,结合了模板)

在C++Primer Plus中说道:

对大多数程序员来说,右值引用带来的主要好处并非是让他们能够编写写使用右值引用的代码,而是能够使用,利用右值引用实现移动语义的库代码,比如说,更好的使用和理解标准库容器等。