C++概念:引用折叠、模板推导、通用引用

2,750 阅读5分钟

1. 引用折叠规则

  1. 如果间接的创建一个引用的引用,则这些引用就会“折叠”(空格前为模板T类型)。
    • X& &、X& &&、X&& &都折叠成X&
    • X&& &&折叠为X&&
  2. 当将一个左值传递给一个参数是右值引用指向模板类型参数(T&&)时,编译器推断模板参数类型为实参的左值引用
template<typename T> 
void f(T&&); 

int i = 42;
f(i) //f函数被推导成:void f<int&>(int&)
  1. 虽然不能隐式的将一个左值转换为右值引用,但是可以通过static_cast显示地将一个左值转换为一个右值

2. 模板推导

模板的类型推导规则还是蛮复杂的,这里简单描述一下,例如:

template <typename T>
void f(ParamType param);

f(expr);

上面这个例子是函数模板的通用例子,其中T是根据f函数的参数推导出来的,而ParamType则是根据 T 推导出来的。T与ParamType有可能相等,也可能不等,因为ParamType是可以加修饰的。根据ParamType类型可以分为如下几类:

  1. ParamType是值类型:因为是值传递,所以expr的所有修饰特性都会被忽略,const, 引用,volatile等,都被忽略。
template<typename T>
void f(T param); // param 为值传递
f(expr);

int x = 2;
const int cx = x;
const int& rx = x;
int *pp = &x;      
f(x); // T : int, param : int
f(cx); // T : int, param : int
f(rx); // T : int , param : int
f(pp); // T是int*,指针比较特殊,直接使用
  1. ParamType是指针或者引用但不是万能引用:ParamType是指针或引用时,引用特性在推导过程中是被忽略的
template <typename T>
void func(T& param);
int x = 10;         // x是int
int & rr = x;       // rr是 int &
const int cx = x;   // cx是const int
const int& rx = x;  // rx是const int &
int *pp = &x;      
func(x);            // T为int
func(cx);           // T为const int
func(rx);           // T为const int
func(rr);           // T为int
func(pp);           // T是int*,指针比较特殊,直接使用
//当ParamType是指针或引用时,引用特性在推导过程中是被忽略的。


template<typename T>
void f(const T& param);
int x = 1;
const int cx = x;
const int &rx = x;

f(x); // T的类型是int, param类型是const int&
f(cx); // T的类型是int,param类型是const int&
f(rx); // T的类型是int,param类型是const int&
//当ParamType是指针或引用时,引用特性在推导过程中是被忽略的。
// 同理,由于param已经具有const特性,所以在推导过程中const属性也会被忽略。

template<typename T>
void f(T* param);
int x = 1;
const int *px = &x;
f(&x); // T的类型int, param类型int*
f(px);  // T的类型是const int , param类型const int*
  1. ParamType是通用引用类型

template<typename T>
void f(T&& param);
f(expr);
// 如果expr是个左值,则T和paramtype都会推导为左值引用
// 如果expr是个右值,正常推导
int x = 2;
const int cx = x;
const int& rx = x;

f(x); // x 是左值, T的类型为int&, param 为 int&
f(cx); cx : lvalue, T : const int&, param: const int&
f(rx);rx : lvalue, T: const int&, param: const int&
f(2);2: rvalue, T: int, param : int &&
  1. param 的类型是最完整的类型,继承了形参中声明的 cr(const 和 reference)和实参总带过来的 cr 。但有两个特例:
  • 当形参时通用引用(T&&作为模板参数时称为通用引用)时, param 根据具体的实参类型,推导为左值引用或者右值引用;
  • 当形参不是引用时,实参到形参为值传递,去除所有 cr 修饰符。
  1. T 中是否包含 cr 修饰符,取决于 param 的修饰符是否已在形参中声明过。即 T 中修饰符不会与形参中已声明的修饰符重复。

3. typename remove_reference<T>::type&&类型

C++11 以前,类成员有成员函数、成员变量、静态成员三种类型,但从 C++11 之后又增加了一种成员称为类型成员。类型成员与静态成员一样,它们都属于类而不属于对象,访问它时也与访问静态成员一样用::访问。

template <typename T>
struct remove_reference{
    typedef T type;  //定义T的类型别名为type
};

template <typename T>
struct remove_reference<T&> //左值引用
{
    typedef T type;
}

template <typename T>
struct remove_reference<T&&> //右值引用
{
   typedef T type;
}

通过上面的代码我们可以知道,经过 remove_reference 处理后, T 的引用被剔除了。假设前面我们通过 move 的类型自动推导得到 Tint&&,那么再次经过模板推导 remove_referencetype 成员,这样就可以得出 type 的类型为 int

4. std::movestd::forward解析

4.1. std::move解析

标准库中move的定义如下:

template<typename T>
typename remove_reference<T>::type && move(T&& t)
{
    return static_cast<typename remove_reference<T>::type &&>(t);
}

move 函数的参数 T&&是一个指向模板类型参数的右值引用规则,通过引用折叠,此参数可以和任何类型的实参匹配,因此 move 既可以传递一个左值,也可以传递一个右值。

4.1.1. 例子

std::move(string("hello")) 调用解析:

  • 首先,根据模板推断规则,确地 T 的类型为 string ;
  • typenameremove_reference<T>::type 的结果为 string ;
  • move 函数的参数类型为 string&&;
  • static_cast<string&&>(t)t 已经是 string&&,于是类型转换什么都不做,返回 string&&;

strings1("hello"); std::move(s1);调用解析:

  • 首先,根据模板推断规则,确定 T 的类型为 string&;
  • typenameremove_reference<T>::type 的结果为 string
  • move 函数的参数类型为 string&&&,引用折叠之后为 string&;
  • static_cast<string&&>(t)tstring&,经过 static_cast 之后转换为 string&&,返回 string&&;

move的定义可以看出,move自身除了做一些参数的推断之外,返回右值引用本质上还是靠static_cast<T&&>完成的。因此下面两个调用是等价的,std::move就是个语法糖。

void func(int&& a)
{
    cout << a << endl;
}

int a = 6;
func(std::move(a));

int b = 10;
func(static_cast<int&&>(b));   

4.2. std::forward解析

template<typename T> 
T&& forward(typename remove_reference<T>::type& param) 
{
    return static_cast<T&&>(param);
}

本质与move一样,都是靠static_cast<>强制转换来完成

5. 通用引用

既然可以接收左值,右可以接收右值。例如move的输入参数。通用类型有两种方式:

  • typename remove_reference<T>::type &&模板方法
  • auto &&,auto实际是模板中的T
#include <iostream>
template<typename T>
void f(T&& param){
    std::cout << "the value is "<< param << std::endl;
}
int main(int argc, char *argv[]){

    int a = 123;
    auto && b = 5;   //通用引用,可以接收右值

    int && c = a;    //错误,右值引用,不能接收左值

    auto && d = a;   //通用引用,可以接收左值

    const auto && e = a; //错误,加了const就不再是通用引用了

    func(a);         //通用引用,可以接收左值
    func(10);        //通用引用,可以接收右值
}

6. 参考文献

C++高阶知识:深入分析移动构造函数及其原理
高速上手C++11/C++14
深入浅出 C++ 11 右值引用
聊聊C++中的完美转发