引用折叠

在这篇[文章]提到了通用引用,当一个左值作为通用引用模板实参时,模板形参推导为左值引用,当一个右值作为通用引用模板实参时,模板形参推导为右值引用, 那么这个推导过程依赖于编码机制和引用折叠

这个编码机制概况下来就是:如果左值作为通用引用模板实参,T的推导结果就是左值引用型别,当一个右值作为通用引用模板实参时,T的推导结果就是非引用型别

注意加粗部分说的是T的推导结果,上面提到的是模板形参最终推导结果,我们可以尝试将T的结果代入模板,例如:

template<typename T>
void fun(T&& param)
{
	someFun(std::forward<T>(param));
}

Widget MakeWidget();

Widget objWidget;

fun(objWidget);
fun(MakeWidget());
  • fun(objWidget)调用

    objWidget是个左值,按照编码机制T是Widget&,那么模板变成这样:

      void fun(Widget& && param)
    
  • fun(MakeWidget())调用

    MakeWidget()返回一个右值,按照编码机制T是Widget,那么模板变成这样:

      void fun(Widget&& param)
    

先声明,在C++中引用的引用是不合法的,例如刚刚提到的模板void fun(Widget& && param),不合法只是对用户不合法,编译器内部是允许的。最终又通过引用折叠技术将这个函数签名变成这样:

void fun(Widget& && param)

// 引用折叠技术
void fun(Widget& param)

引用折叠技术就是将双重引用折叠为单个引用,规则为:如果任一引用为左值引用,则结果为左值引用,否则为右值引用

在谈std::forward


引用折叠技术是使std::forward得以运作的关键,关于std::forward更多可以参考这篇文章,上面提到编码机制会将实参是左值还是右值信息编码到 形参T中,std::forward的任务是:当且仅当编码T中的信息表明了实参是个右值,即T的推导结果型别是个非引用型别时,对传递给std::forward的实参(左值)实施到右值的强制型别转换, 我们可以先看看std::forward的简单实现:

template<typename T>
T&& forward(std::remove_reference_t<T>& fparam)
{
	return static_cast<T&&>(fparam);
}

那么我们在看看上面提到的例子中使用forward时,不同的调用对forward会产生什么效果:

  • fun(objWidget)调用

    objWidget是个左值,按照编码机制T是Widget&,那么foward模板变成这样:

      Widget& && forward(std::remove_reference_t<Widget&>& fparam)
      {
          return static_cast<Widget& &&>(fparam);
      }
    

    使用引用折叠技术和remove_reference_t后:

      Widget& forward(Widget& fparam)
      {
          return static_cast<Widget&>(fparam);
      }
    

    结果就是接受一个左值引用,返回了一个左值引用,std::forward内部的强制型别转换未做任何事情,因为前后型别一致

    传递给std::forward的左值实参返回一个作用左值引用,左值引用是个左值,所以传递左值给std::forward会导致返回一个左值,这也正是我们期望的

  • fun(MakeWidget())调用

    MakeWidget()返回一个右值,按照编码机制T是Widget,那么foward模板变成这样:

      Widget&& forward(std::remove_reference_t<Widget>& fparam)
      {
          return static_cast<Widget&&>(fparam);
      }
    

    使用remove_reference_t后:

      Widget&& forward(Widget& fparam)
      {
          return static_cast<Widget&&>(fparam);
      }
    

    这里并没有发生引用折叠,由于函数返回的是右值引用,即右值,std::forward会把Fun的形参param(左值)转换为右值,最后传递给函数Fun的右值实参作为右值继续向后传递,同样, 这也正是我们期望的

用到引用折叠技术的场景


使用到引用折叠技术的场景有4种:

  • 上面提到的模板实例化
  • auto变量的型别生成
  • 生成、使用typedef和别名声明
  • decltype

auto变量的型别生成技术本质上和模板实例化一样,利用上面提到的例子进一步说明:

auto&& w1 = objWidget;
auto&& w2 = MakeWidget();
  • 第一个声明

    objWidget是个左值,auto的型别推导为Widget&,代入后:

      Widget& && w1 = objWidget;
    

    引用折叠后:

      Widget& w1 = objWidget;
    

    w1的类型为左值引用

  • 第二个声明

    函数MakeWidget()返回一个右值,auto的型别推导为非引用型别Widget,代入后:

      Widget&& w2 = MakeWidget();
    

    w2的类型为右值引用

引用折叠技术在typedef中也用到了,例如:

tempate<typename T>
class Widget
{
public:

	typedef T&& DefTypeName;
}

这个例子中当T是int&等类似的形式,就会发生引用折叠

decltype亦然

最后


  • 通用引用会在型别推导的过程中区别左值和右值,以及会发生引用折叠语境中的右值引用

  • 当编译器在引用折叠语境下生成引用的引用时,结果会变成单个引用,如果原始的引用中有任一引用为左值引用,则结果为左值引用,否则结果为右值引用

  • 引用折叠会发生在4种语境中:模板实例化,auto型别生成,创建、使用typedef和别名声明,decltype