c++11新特性之右值引用

       C++11 增加了一个新的类型,称为右值引用(R-value reference),标记为 T &&。在介绍右值引用类型之前先要了解什么是左值和右值。左值是指表达式结束后依然存在的持久对象,右值是指表达式结束时就不再存在的临时对象。一个区分左值与右值的便捷方法是:看能不能对表达式取地址,如果能,则为左值,否则为右值 。所有的具名变量或对象都是左值,而右值不具名。

       在 C++11 中,右值由两个概念构成,一个是将亡值(xvalue, expiring value),另一个则是纯右值(prvalue, PureRvalue),比如,非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等都是纯右值。而将亡值是 C++11 新增的、与右值引用相关的表达式,比如,将要被移动的对象、 T&& 函数返回值、 std::move 返回值和转换为 T&&的类型的转换函数的返回值。

      C++11 中所有的值必属于左值、将亡值、纯右值三者之一 ,将亡值和纯右值都属于右值。区分表达式的左右值属性有一个简便方法:若可对表达式用 & 符取址,则为左值,否则为右值。

      右值引用就是对一个右值进行引用的类型。因为右值不具名,所以我们只能通过引用的方式找到它。

      无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。

class A_2
{
public:
	A_2() :m_ptr(new int(0))
	{
 
	}
 
	~A_2()
	{
		delete m_ptr;
	}
 
private:
	int* m_ptr;
};
 
A_2 Get(bool flag)
{
	A_2 a;
	A_2 b;
	if (flag)
		return a;
	else
		return b;
}
 
int right_value_ref2()
{
	A_2 a = Get(false);  //运行报错
 
	return 0;
}

       在上面的代码中,默认构造函数是浅拷贝,a和b会指向同一指针m_ptr,在析构函数会导致重复删除该指针。

       正确的做法是提供深拷贝的拷贝构造函数。

A_2(const A_2& a):m_ptr(new int(*a.m_ptr)) //深拷贝
{
	std::cout << "copy construct" << std::endl;
}
//这样就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,不人上面代码中的拷贝构造就是不必要的。上面代码中的Get函数会返回临时变量,
//然后通过这个临时变量拷贝构造一个新的对象b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。
//有没有办法避免临时对象的拷贝构造呢?答案是肯定的。看下面的代码:
class A_2
{
public:
	A_2() :m_ptr(new int(0))
	{
		std::cout << "construct" << std::endl;
	}
 
	A_2(const A_2& a):m_ptr(new int(*a.m_ptr)) //深拷贝
	{
		std::cout << "copy construct" << std::endl;
	}
 
	A_2(A_2&& a) :m_ptr(a.m_ptr)
	{
                // 这一步很关键,真正实现资源拥有权的转移,如果去掉这一步,a.m_ptr在移动构造函数结束时候执行析构函数
                // 会将我们偷来的内存析构掉。a.m_ptr会变成悬垂指针。如果我们对指针解引用,就会发生严重的运行错误
		a.m_ptr = nullptr; 
		std::cout << "move construct:" << std::endl;
	}
 
	~A_2()
	{
		std::cout << "destruct" << std::endl;
		delete m_ptr;
	}
 
private:
	int* m_ptr;
};
 
A_2 Get(bool flag)
{
	A_2 a;
	A_2 b;
	if (flag)
       return a;
	else
       return b;
}
 
int right_value_ref2()
{
	A_2 a = Get(false);
 
	return 0;
}

 

       上面的代码中没有了拷贝构造,取而代之的是移动构造(Move Construct)。从移动构造函数的实现可以看到,它的参数是一个右值引用类型的参数A&&,这里没有深拷贝,只有浅拷贝,这样就避免了临时对象的深拷贝,提供了性能。这里的A&&用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义(move 语义),右值引用的一个重要目的是用来支持移动语义的。值得注意的是移动构造里面将a.m_ptr置为nullptr(注释写得很清楚),此时delete不会对nullptr做出任何操作。

       移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高C++应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响

 

再看一个简单的例子,代码如下:

struct Element
{
    Element(){}
    // 右值版本的拷贝构造函数
    Element(Element&& other) : m_children(std::move(other.m_children)){}
    Element(const Element& other) : m_children(other.m_children){}
private:
    vector<ptree> m_children;
};

 

这个 Element 类提供了一个右值版本的构造函数。这个右值版本的构造函数的一个典型

应用场景如下:

void Test()
{
    Element t1 = Init();
    vector<Element> v;
    v.push_back(t1);
    v.push_back(std::move(t1));
}

 

       先构造了一个临时对象 t1,这个对象中一个存放了很多 Element 对象,数量可能很多,如果直接将这个 t1 用 push_back 插入到 vector 中,没有右值版本的构造函数时,会引起大量的拷贝,这种拷贝会造成额外的严重的性能损耗。通过定义右值版本的构造函数以及

std::move(t1) 就可以避免这种额外的拷贝,从而大幅提高性能。

       有了右值引用和移动语义,在设计和实现类时,对于需要动态申请大量资源的类,应该设计右值引用的拷贝构造函数和赋值函数,以提高应用程序的效率。需要注意的是,我们一般在提供右值引用的构造函数的同时,也会提供常量左值引用的拷贝构造函数,以保证移动不成还可以使用拷贝构造。

      这里也要注意对 move 语义的误解, move 只是转移了资源的控制权,本质上是将左值强制转换为右值引用,以用于 move 语义,避免含有资源的对象发生无谓的拷贝。 move 对于拥有形如对内存、文件句柄等资源的成员的对象有效。如果是一些基本类型,比如 int 和char[10] 数组等,如果使用 move,仍然会发生拷贝(因为没有对应的移动构造函数),所以说move 对于含资源的对象来说更有意义。

      还要注意的是,右值引用不能绑定左值:int a;  int &&c = a;   这样是不行的。

 

另附一段右值引用性能测试的代码:


#define  MAX_TIMES 1000

class A {
public:
	A(const char *pstr) {
		m_data = (pstr != 0 ? strcpy(new char[strlen(pstr) + 1], pstr) : 0);
	}

	A(const A &a) {
		m_data = (a.m_data != 0 ? strcpy(new char[strlen(a.m_data) + 1], a.m_data) : 0);
	}

	A &operator =(const A &a) {
		if (this != &a) {
			delete[] m_data;
			m_data = (a.m_data != 0 ? strcpy(new char[strlen(a.m_data) + 1], a.m_data) : 0);
		}
		return *this;
	}

	A(A &&a) : m_data(a.m_data) {
		a.m_data = 0;
	}

	A & operator = (A &&a) {
		if (this != &a) {
			m_data = a.m_data;
			a.m_data = 0;
		}
		return *this;
	}

	~A() { delete[] m_data; }
private:
	char * m_data;
};

//移动语义实现
void swap1(A& a, A& b)
{
	A tmp(move(a));
	a = (move(b));
	b = move(tmp);
}

// 普通交换
void swap2(A& a, A& b)
{
	A tmp = a;
	a = b;
	b = tmp;
}

int main()
{
	A a("123"), b("456");
	clock_t start, finish, start1, finish1;
	double totaltime;
	start = clock();

	for (int i = 0; i < MAX_TIMES; ++i)
	{
            swap1(a, b);
	}

	finish = clock();
	totaltime = (double)(finish - start) / CLOCKS_PER_SEC;
	cout << "MAX_TIMES =" << MAX_TIMES << "\n右值引用的运行时间为" << totaltime << "秒!" << endl;

	start1 = clock();

	for (int i = 0; i < MAX_TIMES; ++i)
	{
            swap2(a, b);
	}

	finish1 = clock();
	totaltime = (double)(finish1 - start1) / CLOCKS_PER_SEC;
	cout << "MAX_TIMES =" << MAX_TIMES << "\n普通的运行时间为" << totaltime << "秒!" << endl;

        cin.get();

        return 0;
}

 

测试结果:

10000次以下时几乎没有差别,这里不列出;

 

通过大量的测试表明,右值引用内比普通的构造性能确实要高,我这边测试大约是15倍的样子。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值