# 模板中的实参推断和引用

# 从左值引用函数参数推断类型

  • 编译器会应用正常的引用绑定规则
  • const是底层的不是顶层的
template<typename T>
void f1(T &t)
{
    std::cout << t << std::endl;
}

void main(void)
{
    int a = 100;
    f1(a); // T = int
    const int ci = 100;
    f1(ci); // T = const int
    f1(10); // error
}

在上面代码中,模板函数f1参数是一个左值引用T&, 因此只能接受一个左值,可以是const类型,但不能是一个右值。

若模板函数参数是常引用const T&,则函数参数可以传入给它任何类型的实参,一个对象const或非const、临时对象、字面常量。

template<typename T>
void f1(const T &t)
{
    std::cout << t << std::endl;
}

void main(void)
{
    int a = 100;
    f1(a); // T = int
    const int ci = 100;
    f1(ci); // T = const int
    f1(10); // OK
}

# 从右值引用函数参数推断类型

template <typename T>
void f2(T &&t)
{ }
void main(void) {f2(10); }// T = int

如上的右值引用参数函数,当给其传入右值实参进行函数实例化时,会按右值引用传入。

但对于上面的函数f2,会惊奇的发现竟然能够传入左值,

void main(void) {
    int a = 10;
    f2(a); // T = int&
}

如果对于普通的非模板函数f2(a)会立刻提示语法错误,但对于模板函数,定义了两个例外的编译规则

第一个编译规则,当将一个左值传递给模板函数的模板类型右值引用的参数时,编译器推断模板类型参数为实参的左值引用类型。所以调用f2(a)时,T被推断成了int&而非int

T被推断成int&,f2的参数好像被推断成了int& &&,貌似是一个int&类型的右值引用。在C++中不能直接定义引用的引用,但是就像这样,有可能间接产生引用的引用。为了处理间接定义的引用的引用,C++中又定义了第二个例外的编译规则,当间接定义了引用的引用时,这些引用会形成折叠。T& &/T& &&/T&& &都会被折叠成T&T&& &&被折叠成T&&

上面的例子,当定义接受右值引用参数的模板函数时,因为即能传入左值实例化成T&,又能传入右值,会导致意向不到的效果。

template<typename T>
T fcn(T t)
{
    return T + 1
}

template <typename T>
void f2(T &&t)
{ 
    T val = t; // 这里是把值t赋值给val, 还是左值的引用别名呢?
    val = fcn(val); // 赋值给`val`会不会改变t的值呢?
}

void main(void)
{
    int a = 10;
    f2(a); // 这时是`T=int&`
    f2(10); // `T=int
}

如此就会导致函数的歧义,编写模板函数时处理起来就很麻烦。实际中,右值引用的使用情况有两种,一个是模板转发其实参,就是将参数原封不动的传给其调用的函数;另一种是模板被重载。

解决这个问题的一个方法是利用函数重载,写两个模板函数,如:


// a 可变右值引用调用
template<typename T>
void f1(T &&t)
{
    int a = 100;
    t = 100;
    std::cout << t << std::endl;
}

// b 左值引用或const右值引用调用
template<typename T>
void f1(const T&t)
{
    int a = 100;
    std::cout << t << std::endl;
}
void main(void)
{
    int a = 100;
    const int b = std::move(a);
    f1(10); // 可变右值引用调用a
    f1(b); // const右值引用调用b
    return 0;
}

# std::move--右值引用模板函数的一个实例

std::move函数用来将左值转换成右值。其在标准库中的定义,

template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ 
    return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); 
}

从上面的代码可以看到,move函数的参数是指向模板类型_Tp右值引用。在上一部分的介绍,这种根据引用折叠的规则也可以接受左值。

string s1("cpp"), s2;
s2 = std::move(s1); // case 1, 传入左值,`s1`按引用传入, 导致s1的值不确定
s2 = std::move(string("c")); // case2, 传入右值,从有值移动数据。

case1中是按左值引用传入的s1case2中,向一个右值引用函数参数传入一个右值时,右实参推断出的类型是被引用的类型。

在上面调用std::move(string("c"))时,

  • 根据实参推断的T的类型为string
  • remove_referencestring来实例化
  • 模板结构体remove_reference的成员typestring
  • move的返回类型是string&&
  • move的函数参数__t的类型为string&&

此时move实例后的函数为:

string &&move(string &&)

当传入的参数是左值时,

  • 根据实参推断出的_Tp的类型为string&
  • remove_referencestring&来实例化
  • remove_reference<string&>type成员是string
  • move的返回类型仍然是string&&
  • move参数的类型经由引用折叠变成了string&

此时move实例后的函数为:

string &&move(string &)

remove_reference是一个模板类,并有左值和右值引用的实例模板,可以取出左值右值引用的原始类型。static_cast可以从一个左值转换成右值引用。

# 参数转发

有时候在编写函数模板时会希望,模板函数能将传入的参数原封不动的转发给模板函数被调用的函数,譬如保持参数引用,const,左值还是右值等属性。

看一个模板函数,其接受一个可调用表达式和两个额外实参,

void fun1(int a, int &b)
{
    std::cout << a << std::endl << ++b << std::endl;
}

template<typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
    fun1(t2, t1);
}

void main(void)
{
    int t1 = 10;
    flip1(fun1, t1, 10);
    std::cout << "t1: " << t1 << std::endl; // t1: 10
}

调用函数后会发现t1的值没有能够改变,这是因为模板函数被实例成了

void flip1(void(*fun1)(int, int&), int, int)

可以看到变量在传入函数flip1中时,是按值传递的,而不是引用,因此调用函数flip1传入的边值的值并不会被改变。

有人可能会想到前面的右值引用的模板函数可以接受左值,而且被折叠成了左值引用,不是刚好可以用吗?

void fun1(int a, int &b)
{
    std::cout << a << std::endl << ++b << std::endl;
}

template<typename F, typename T1, typename T2>
void flip1(F f, T1 &&t1, T2 &&t2)
{
    fun1(t2, t1);
}

void main(void)
{
    int t1 = 10;
    flip1(fun1, t1, 10);
    std::cout << "t1: " << t1 << std::endl; // t1: 11
}

通过上面的代码可以发现,确实t1是按左值引用传入的,t1的值被变成了11

但是,对于接受右值引用参数的函数效果怎么样呢? 很遗憾,上面的方法依然存在问题。例如将fun1改成接受右值引用参数的函数,

void fun1(int &&a, int &b)
{
    std::cout << a << std::endl << ++b << std::endl;
}

template<typename F, typename T1, typename T2>
void flip1(F f, T1 &&t1, T2 &&t2)
{
    /**
     * 编译报错,传入10,t2是右值引用,t2是一个变量是一个左值,
     * 使用左值调用接受右值引用的函数,编译会报错
     */ 
    fun1(t2, t1); 
}

void main(void)
{
    int t1 = 10;
    flip1(fun1, t1, 10);
    std::cout << "t1: " << t1 << std::endl; 
}

上面的代码将无法通过编译,因为flip1的参数t2是左值,类型是int&&,无法直接传递给fun1函数。

在标准库中有std::forward模板函数,专门用来做函数的参数转发,可以保持原始实参的类型。通过其返回类型上的引用折叠,forward可以保持给定实参的左值右值属性。

通过在模板函数中使用std::forward函数,就可以实现上面的代码了,

void fun1(int &&a, int &b)
{
    std::cout << a << std::endl << ++b << std::endl;
}

template<typename F, typename T1, typename T2>
void flip1(F f, T1 &&t1, T2 &&t2)
{
    fun1(std::forward<T2>(t2), std::forward<T1>(t1)); 
}

void main(void)
{
    int t1 = 10;
    flip1(fun1, t1, 10);
    std::cout << "t1: " << t1 << std::endl; 
}