右值引用与移动:
C++11中引用了右值引用和移动语义,可以避免无谓的复制,提高了程序性能。
区分表达式的左右值属性:如果可对表达式用&符取址,则为左值,否则为右值。
在进入正题之前,首先得区分什么是左右值:
左值lvalue 是有标识符、可以取地址的表达式,最常见的情况有:
**变量、函数或数据成员的名字
**返回左值引用的表达式,如 ++x、x = 1、cout << ' '
**字符串字面量如 "hello world"
纯右值prvalue 是没有标识符、不可以取地址的表达式,一般也称之为“临时对象”。最常见的情况有:
**返回非引用类型的表达式,如 x++、x + 1、make_shared(42)
**除字符串字面量之外的字面量,如 42、true
C++11为我们提供了一种能引用右值的办法:
1. &&:
&&: 左右值都可以引用
& : 肯定是左值引用
右值引用就是对一个右值进行引用的类型。因为右值没有名字,所以我们只能通过引用的方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所把绑定对象的内存,只是该对象的一个别名。
特点如下:
1. 左值和右值是独立于它们的类型的,右值引用类型可能是左值也可能是右值。
2. auto&& 或函数参数类型自动推导的 T&& 是一个未定的引用类型,被称为 universal references,它可能是左值引用也可能是右值引用类型,取决于初始化的值类型。
3. 所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引用。当 T&& 为模板参数时,输入左值,它会变成左值引用,而输入右值时则变为具名的右值引用。
4. 编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值。
特点理解起来不是很容易,先看一下右值引用:
#include <iostream>
using namespace std;
class A
{
public:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
~A(){
cout << "destructor A, m_ptr:" << m_ptr << endl;
delete m_ptr;
m_ptr = nullptr;
}
private:
int* m_ptr;
};
// 为了避免返回值优化(删除保持函数返回值的临时对象),此函数故意这样写???
A Get(bool flag)
{
A a;
A b;
cout << "ready return" << endl;
if (flag)
return a;
else
return b;
}
int main()
{
{
A a = Get(false);
}
cout << "main finish" << endl;
return 0;
}
在上面的代码中,默认构造函数是浅拷贝(浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。),main函数的 a 和Get函数的 b 会指向同一个指针 m_ptr,在析构的时候会导致重复删除该指针。
在看懂这个代码情况下,运行这个代码,观察拷贝的过程:

可以发现这里比理论上的destructor次数多一次,说明我们构造的指针比容器数量多,存在多个指针指向同一个内存,所以为了每一个指针拥有一个独立的指针,我们构造函数改为这样,其他不变:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
A(const A& a) :m_ptr(new int(*a.m_ptr)) {
cout << "copy constructor A" << endl;
} //保证复制是深度复制
改进之后,这样就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,比如上面代码中的拷贝构造就是不必要的。上面代码中的 Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象 b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。有没有办法避免临时对象的拷贝构造呢?答案是肯定的。看下面的代码:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
A(const A& a) :m_ptr(new int(*a.m_ptr)) {
cout << "copy constructor A" << endl;
}
A(A&& a) :m_ptr(a.m_ptr) {//A&& 用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数
a.m_ptr = nullptr;
cout << "move constructor A" << endl;上面的代码中没有了拷贝构造,取而代之的是移动构造( Move Construct)。从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数 A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的 A&& 用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义( move 语义),右值引用的一个重要目的是用来支持移动语义的。
移动语义:

1. move/forward:
move: 左值变右值
forward: (1)不是(2)传入,就是左值变右值
(2)T &&t 根据原来的值判断。
#include <iostream>
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <string.h>
using namespace std;
class MyString {
private:
char* m_data;
size_t m_len;
void copy_data(const char *s) {
m_data = new char[m_len+1];
memcpy(m_data, s, m_len);
m_data[m_len] = '\0';
}
public:
MyString() {
m_data = NULL;
m_len = 0;
}
MyString(const char* p) {
m_len = strlen (p);
copy_data(p);
}
MyString(const MyString& str) {
m_len = str.m_len;
copy_data(str.m_data);
std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl;
}
MyString& operator=(const MyString& str) {
if (this != &str) {
m_len = str.m_len;
copy_data(str.m_data);
}
std::cout << "Copy Assignment is called! source: " << str.m_data << std::endl;
return *this;
}
// 用c++11的右值引用来定义这两个函数
MyString(MyString&& str) {
std::cout << "Move Constructor is called! source: " << str.m_data << std::endl;
m_len = str.m_len;
m_data = str.m_data; //避免了不必要的拷贝
str.m_len = 0;
str.m_data = NULL;
}
MyString& operator=(MyString&& str) {
std::cout << "Move Assignment is called! source: " << str.m_data << std::endl;
if (this != &str) {
m_len = str.m_len;
m_data = str.m_data; //避免了不必要的拷贝
str.m_len = 0;
str.m_data = NULL;
}
return *this;
}
virtual ~MyString() {
if (m_data) free(m_data);
}
};
int main()
{
MyString a;
a = MyString("Hello"); // move
MyString b = a; // copy
MyString c = std::move(a); // move, 将左值转为右值
return 0;
}移动语义是通过右值引用来匹配临时值的,那么,普通的左值是否也能借组移动语义来优化性能呢?C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语义。move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。
———————————————————————————————————————————
forward 完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值(用途比move广)。
int &&a = 10;
int &&b = a; //错误
int &&a = 10;
int &&b = std::forward<int>(a);减少内存拷贝和移动的应用:
利用减少拷贝和移动的原理,实现了一些优化之后的函数:
1. emplace_back
emplace_back是就地构造,不用构造后再次复制到容器中。因此效率更高。
测试用例:
time_interval.h
#ifndef TIME_INTERVAL_H
#define TIME_INTERVAL_H
#include <iostream>
#include <memory>
#include <string>
#ifdef GCC
#include <sys/time.h>
#else
#include <ctime>
#endif // GCC
class TimeInterval
{
public:
TimeInterval(const std::string& d) : detail(d)
{
init();
}
TimeInterval()
{
init();
}
~TimeInterval()
{
#ifdef GCC
gettimeofday(&end, NULL);
std::cout << detail
<< 1000 * (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec) / 1000
<< " ms" << endl;
#else
end = clock();
std::cout << detail
<< (double)(end - start) << " ms" << std::endl;
#endif // GCC
}
protected:
void init() {
#ifdef GCC
gettimeofday(&start, NULL);
#else
start = clock();
#endif // GCC
}
private:
std::string detail;
#ifdef GCC
timeval start, end;
#else
clock_t start, end;
#endif // GCC
};
#define TIME_INTERVAL_SCOPE(d) std::shared_ptr<TimeInterval> time_interval_scope_begin = std::make_shared<TimeInterval>(d)
#endif // TIME_INTERVAL_H
main.cpp:
#include <vector>
#include <string>
#include "time_interval.h"
int main() {
std::vector<std::string> v;
int count = 10000000;
v.reserve(count); //预分配十万大小,排除掉分配内存的时间
{
TIME_INTERVAL_SCOPE("push_back string:");
for (int i = 0; i < count; i++)
{
std::string temp("ceshi");
v.push_back(temp);// push_back(const string&),参数是左值引用
}
}
v.clear();
{
TIME_INTERVAL_SCOPE("push_back move(string):");
for (int i = 0; i < count; i++)
{
std::string temp("ceshi");
v.push_back(std::move(temp));// push_back(string &&), 参数是右值引用
}
}
v.clear();
{
TIME_INTERVAL_SCOPE("push_back(string):");
for (int i = 0; i < count; i++)
{
v.push_back(std::string("ceshi"));// push_back(string &&), 参数是右值引用
}
}
v.clear();
{
TIME_INTERVAL_SCOPE("push_back(c string):");
for (int i = 0; i < count; i++)
{
v.push_back("ceshi");// push_back(string &&), 参数是右值引用
}
}
v.clear();
{
TIME_INTERVAL_SCOPE("emplace_back(c string):");
for (int i = 0; i < count; i++)
{
v.emplace_back("ceshi");// 只有一次构造函数,不调用拷贝构造函数,速度最快
}
}
}
可以得到:

可以看出,我们在建立程序时,应该优先考虑emplace_back函数。