Home

cuipf blog

22 Jul 2017

C++11特性介绍——右值引用、移动操作、完美转发

左值、右值和右值引用

关于左右值判断有两种说法:

  1. 在赋值表达式中,出现在等号左边的就是“左值”,而在等号右边的,则称为“右值”;
  2. C++还有一种说法,可以取地址的,有名字的就是左值,反之,不能取地址的,没有名字就是右值。
a = b + c; a是一个左值,(b + c)是一个右值。
int i = 0;
int *ip = &(i++); //错误 所以i++为右值
int *ip = &(++i); //正确 ++i为左值

右值又分为两种:

  1. 将亡值: C++11中新增的跟右值引用相关的表达式,这样的表达式通常是将要被移动的对象。
  2. 纯右值:用于辨识临时变量和一些不跟对象关联的值。

右值引用:移动语义和完美转发

指针成员和拷贝构造

当类中含有指针成员时候,拷贝构造函数就需要自己实现,不能使用默认的拷贝构造函数(浅拷贝),否则会造成指针悬挂的危险。存在指针成员的类, 在实现构造函数时候一定要格外小心;

移动语义

先看下面的代码:

// 移动语义
class HasPtrMem
{
public:
	HasPtrMem(): d_(new int(0))
	{
		cout << "Construct: " << ++n_cstr_ << endl;
	}
	HasPtrMem(const HasPtrMem& h) : d_(new int(*h.d_))
	{
		cout << "Copy construct: " << ++n_cptr_ << endl;
	}

	~HasPtrMem()
	{
		cout << "Destruct: " << ++n_dstr_ << endl;
		delete d_;
	}

public:
	int* d_;
	static int n_cstr_;
	static int n_dstr_;
	static int n_cptr_;
	static int n_mvtr_;
};

int HasPtrMem::n_cstr_ = 0;
int HasPtrMem::n_dstr_ = 0;
int HasPtrMem::n_cptr_ = 0;
int HasPtrMem::n_mvtr_ = 0;

HasPtrMem GetTemp()
{
	return HasPtrMem();
}

void test()
{
    HasPtrMem mem = GetTemp();
}

执行test函数,会输出什么?

Construct: 1
Destruct: 1

加上编译选项 -fno-elide-constructors 会输出什么?

Construct: 1
move construct: 1
Destruct: 1
move construct: 2
Destruct: 2
Destruct: 3

很明显,默认情况下编译器都会进行 返回值优化 也叫RVO优化,有效的提高效率。 在C++ 11中我们还有另外一种方式来优化这种临时对象拷贝问题,就是移动构造函数;实现如下: 在上面类中加入移动构造函数:

//移动构造函数
HasPtrMem(HasPtrMem && h) : d_(h.d_)
{
	h.d_ = nullptr;     //将实参传递进来变量的指针成员置空
	cout << "move construct: " << ++n_mvtr_ << endl;
}

void test_move()
{
	HasPtrMem mem_move = GetTempMove();
	cout << "Resource from " << __func__ << ":" << hex << mem_move.d_ << endl;
}

打印输出如下:

Construct: 1
Resource from GetTempMove:0x604010
move construct: 1
Destruct: 1
move construct: 2
Destruct: 2
Resource from TestChaperThree:0x604010
Destruct: 3

移动构造函数,使用d__ 指向h.d _ 所指向的内存,然后把h.d_设置为指针空值,防止析构时候出现错误;

注: 移动构造函数什么时间会触发呢? 一旦使用到临时对象,移动构造函数就可以被执行了。

右值引用

C++ 11中,所有的值必属于左值,纯右值,将亡值三者之一;纯右值和将亡值都属于右值;将亡值是C++ 11中新增加的跟右值引用相关的表达式,这种表达式通常是将要移动的对象。比如:右值引用T&&的函数返回值,std::move的返回值或者转为T&&类型转换函数的返回值。

在C++ 11中,右值引用就是对一个右值进行引用的类型;右值引用和左值引用都是属于引用类型,无论是声明一个左值引用还是右值引用,都必须立即初始化。其原因可以理解为引用类型本身自己并不拥有所绑定的对象内存,只是该对象的一个别名。

** 注: **

  • 常量的左值引用在C++98中,是一个“万能”的引用,他可以接受非常量左值,常量左值,右值对其进行初始化;
  • 使用右值初始化,常量左值引用的时候,还可以像右值引用一样将右值的声明周期延长;
  • 非常量左值只能接受非常量左值对其进行初始化;
  • 左值持久, 右值短暂, 因为右值要么是字面常量, 要么是在表达式求值过程中创建的临时对象;
  • 右值引用绑定的是即将被销毁的对象, 所以我们可以从绑定到右值引用的对象”窃取”状态;

强制转化为右值

在C++ 11中,提供了一个函数std::move,功能:将一个左值强制转化为右值引用,继而我们可以通过右值引用使用该值,以用于移动语义。

std::move等价于 static_case<T&&>(lvalue);

注意:使用上面转化的左值,生命期并没有随着左右值的转化而改变,也就是并没有析构;

//右值引用
class Moveable
{
public:
	Moveable():i(new int(2))
	{

	}
	~Moveable()
	{
		delete i;
	}
	Moveable(const Moveable& o)
	{
		i = new int(*o.i);
		cout << "Copied" << endl;
	}
	//移动构造函数
	Moveable(Moveable&& mo)
	{
		i = mo.i;
		mo.i = nullptr;
		cout << "move copy " << endl;
	}

public:
	int* i;
};


Moveable a;
Moveable c(move(a)); //会调用移动构造函数
cout << *a.i << endl; //编译运行错误 此时a.i被置为NULL,但是a的声明周期要到函数结束后才结束

上面是一个错误使用方法,被转化的右值a不能再次被使用了,一定要注意;

正确使用move的方式如下:

class HugeMem
{
public:
	HugeMem(int size) : sz(size > 0 ? size : 1)
	{
		c = new int[sz];
	}
	~HugeMem()
	{
		delete[] c;
	}
	//移动拷贝构造函数
	HugeMem(HugeMem && hm) : sz(hm.sz), c(hm.c)
	{
		hm.c = nullptr;
	}
	int* c;
	int sz;
};

class Moveable2
{
public:
	Moveable2() :i(new int(3)), h(1024)
	{

	};
	~Moveable2()
	{
		delete i;
	};
	//强制转为右值,以调用移动构造函数
	Moveable2(Moveable2 && m) :i(m.i), h(move(m.h))
	{
		m.i = nullptr;
	}
	int* i;
	HugeMem h;
};

Moveable2 GetMoveable2Temp()
{
	Moveable2 tmp = Moveable2();
	cout << hex << "Huge Mem from " << __func__ << " @" << tmp.h.c << endl;
	return tmp;
}

//调用
Moveable2 a(GetMoveable2Temp());
cout << hex << "Huge Mem from " << __func__ << " @" << a.h.c << endl;

移动语义的一些其他问题

  1. 移动语义一定会修改临时变量的值,那么如果这样声明移动构造函数:

     Moveable (const Moveable &&);
     const Moveable ReturnVal();
    

    都会使临时变量常量化,成为一个常量右值,那么临时变量的引用也就无法修改,从而导致无法实现移 动语义,要实现移动语义一定要排除不必要的const关键字;

  2. C++ 11中,拷贝/移动构造函数实际上有一下三个版本:

     T object(T & t);
     T object(const T & t) //拷贝构造函数
     T object(T &&t)       //移动构造函数
    
  3. 默认情况下,编译器会隐式的生成一个移动拷贝构造函数,但是如果定义了拷贝构造,拷贝赋值,移动赋值,析构函数中的一个或者多个,编译器都不会生成默认版本。

  4. 所以在C++11中,拷贝构造/赋值和移动构造/赋值函数必须同时提供,或者同时不提供,才能保证类同时具有拷贝/移动语义。
  5. 对于移动构造函数, 如果要用到须自定义;
  6. 移动构造函数尽量编写为不抛出异常的移动构造函数, 否则抛出异常容易形成悬挂指针。可以使用noexcept关键字,保证抛出异常的时候,直接调用terminate程序终止运行,而不是造成指针悬挂。

  7. 在标准库的头文件中里,我们还可以通过一些辅助的模板函数来判断一个类型是否可以移动,比如:

     is_move_constructible
     is_trivially_move_constructible
     is_nothrow_move_constructible
    

    使用方式如下:

     cout << is_move_constructible<UnknowType>::value;
    
  8. 利用std::move实现高性能的swap函数;

     template <class T>
     void Swap(T& a, T& b)
     {
         T tmp(move(a));
         a = move(b);
         b = move(tmp);
     }
    

完美转发

完美转发:是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。 C++ 11通过引入”引用折叠”,并结合新的模板退到规则, 来实现完美转发;

template<typename T>
T&& TestIdentity(T&& t)
{
	return std::forward<T>(t);
}

std::string str("66666");
std::string& str_lref = str;
//使用时候依然是 左值
std::string&& str_rref = std::move(str_lref);  
std::string&& str_rref1 = std::move(str);
//等价于使用std::move
std::string&& str_rref2 = static_cast<std::string&&>(str);
//T: string	传参数:右值引用	返回:右值引用
std::string&& str2 = TestIdentity<std::string>(std::move(str))
//T:  string& 	传参数:左值引用(引用折叠)返回:左值引用
std::string& str3 = TestIdentity<std::string&>(str_lref);        
//T:  string&&  传参数:右值引用 返回:右值引用
std::string&& str4 = TestIdentity<std::string&&>(std::move(str));
//T:  string&  传参数:左值引用 返回:左值引用
std::string& str5 = TestIdentity(str4);	    

显示转换操作符

在C++中有隐式类型转换这个特性,隐私类型转换可以让程序免于层层构造类型,但同样也会带来一些问题。 所以在程序中,一般会使用explicit关键字来保证对象的显示构造,避免出现隐式转换带来的麻烦。

但是,同样的机制并没有出现在自定义的类型转换符上,这样就会出现,自定义类型像已知类型转换!

解决方案 在C++11中,explicit的使用范围扩大到自定义的类型转换操作符上,以支持所谓的“显示类型转换”,explicit关键字作用于类型转换操作符上,意味着只有在直接构造目标类型或隐式类型转换的时候可以使用该类型。

class ConvertTo
{

};

class Convertable
{
public:
	//explicit显示转换到convertto类型类型转换符
	explicit operator ConvertTo () const
	{
		cout << "operator ()" << endl;
		return ConvertTo();
	}
};

void FunCon(ConvertTo ct)
{

}

Convertable c;
ConvertTo ct(c);				//直接初始化
//ConvertTo ct2 = c;			//拷贝构造初始化 编译失败
ConvertTo ct3 = static_cast<ConvertTo>(c); //直接强制转换
//FunCon(c);	                    //拷贝构造初始化失败

cuipf

scribble