通用引用和右值引用

通用引用和右值引用是同一种表达方式——T&&,之所以叫通用引用是因为T&&既可以是右值引用也可以是左值引用,同时可以绑定到const对象、非const对象 volatile对象、非volatile对象、const volatile对象,

通用引用是个引用,所以初始化是必须的,通用引用的初始化物会决定它代表的是个左值引用还是右值引用, 如果初始化物是右值,那么通用引用就会对应一个右值引用,如果初始化物是左值,那么通用引用就会对应一个左值引用

通用引用通常应用于两个场景:

  • 函数模板形参

      template<typename T>
      void Fun(T&& param);
    
  • auto声明

      auto&& var = var1;
    

这两个应用场景都涉及到了类型推导,类型推导是通用引用的必要条件但不是充分条件,其形式也必须是正确的——T&&,下面列举了一些例子来说明通用引用 和右值引用:

//未涉及类型推导,右值引用
void Fun(widget&& param); 

//未涉及类型推导,右值引用
widget&& var = Widget(); 

//param的型别形式不是T&&,而是std::vector<T>&&,右值引用
// 如果传入一个左值触发编译错误——不能给一个右值引用绑定一个左值
template<typename T>
void Fun(std::vector<T>&& param); 

//const剥夺了param成为通用引用的资格
template<typename T>
void Fun(const T&& param);

//param是个通用引用
template<typename T>
void Fun(T&& param); 

//param是个通用引用
template<typename MyType>
void Fun(MyType&& param)

在模板内看到函数形参型别T&&,如果没有类型推导,那么就不是通用引用,例如STL中的vector:

template<class T,class Allocator= allocator<T>>
class vector
{
public:
	void push_back(T&& x);
}

虽然具备了T&&,但是没有类型推导,下面的使用说明了在模板具现化时已经确定了T的类型:

std::vector<widget> v;
class vector<widget,allocator<widget>>
{
	// 未涉及类型推导,右值引用
	void push_back(widget&& x) 
}

与std::vector的push_back语意类似的是emplace_back:

template<class T,class Allocator= allocator<T>>
class vector
{
public:
	
	//args是个通用引用
	template<class... Args>
	void emplace_back(Args&&...args);
}

其中Args(其实它是形参包)是独立于型别参数T;所以Args必须在调用时进行推导

对于当使用auto&&时,其变量都是通用引用,因为它们肯定涉及了类型推导而且有正确的形式——T&&,在C++14的lamda表达式中可以声明auto&&作为形参,

例如统计函数执行时间的函数:

auto ExecTime = [](auto&& funcName,auto&&... params)
{
	开始计时
	std::forward<decltype(funcName)>(funcName)(std::forward<decltype(params)>(params)...);
	停止计时
}

应用


在通用引用上要使用std::forward,从上面得知,一个通用引用不一定绑定到右值,有可能是个左值,例如:

class Widget
{
public:
	template <typename T>
	void setName(T&& param)
	{
		m_name = std::move(param);
	}

private:
	std::string m_name;
};

{
	Widget w;
	std::string strNewName("test");
	w.setName(strNewName);
	...
}

这里的传递给setName实参是个左值,那么在setName调用之后若还对strNewName操作,strNewName就变的未知,产生了错误的行为,进而我们也能得知, 在使用std::move和std::forward时应该保证其后续调用不在对右值引用或者通用引用有任何操作

按值返回的函数

对于一个按值返回的函数,如果返回的是绑定到一个右值引用或一个通用引用的对象,那么要其这个对象使用std::move或者std::forward,例如:

Widget operator+(Widget&& objWidget,const Widget& src)
{
	objWidget += src;
	return std::move(objWidget);
}

template<typename T>
Widget TestForward(T&& param)
{
	return std::forward<T>(param)
}

对于绑定到一个右值引用的对象使用std::move,有两个优点:

  1. 利用了对象的右值性
  2. 扩展性更强

    当Widget不支持移动时,使用std::move不会带来负面影响,当在源代码上提供移动支持时,那么就此例来讲就能自动得到了移动所带来的好处

在按值返回函数中,在实际开发中遇到这样的代码:

Widget TestReturnValue()
{
	Widget objWidget;

	return std::move(objWidget);
}

我们上面提到的是绑定到一个右值引用或一个通用引用的对象,而这个例子是对函数内部创建的临时对象使用了std::move,这样使用会抑制编译器的 返回值优化RVO(return value optimization),关于返回值优化可以参考这篇文章,C++标椎中提到:

编译器若要在一个按值返回的函数里省略对局部对象的复制或者移动,需要满足两个前提条件:

  1. 局部对象型别和函数返回值型别相同
  2. 返回的就是局部对象本身

在开发中遇到的这个代码违背了前提条件的第2条,返回的是个引用

当编译器不执行这个策略时,也必须按照C++的另一标准:

当编译器选择不执行复制省略时,返回对象必须作为右值处理

所以即使不执行这个策略,编译器隐式的把std::move应用于返回值对象上,就像上面的代码

所以针对这段代码有2个选择:

  1. 使用RVO
  2. 直接返回一个按值的形参

最后


  • 如果函数模板形参具备T&&型别,并且T的型别系推导而来,或如果对象使用auto&&声明其型别,则该形参或对象就是个通用引用

  • 如果型别声明并不精确的具备T&&的形式,或者未触发型别推导,则T&&代表右值引用

  • 如果用右值来初始化通用引用,就会得到一个右值引用,如果用左值来初始化通用引用,就会得到一个左值引用

  • 针对右值引用的最后一次使用则使用std::move,针对万能引用的最后一次使用则使用std::forward

  • 对于一个按值返回的函数,如果返回的是绑定到一个右值引用或一个通用引用的对象,那么要其这个对象使用std::move或者std::forward

  • 如果局部对象可能适用于RVO,则不要对其使用std::move或者std::forward