C++模板实参类型推导(引用折叠、右值引用参数)和完美转发

一、模板实参类型推断和引用

1.1 从左值引用参数推断类型

当一个函数参数是模板类型参数的一个普通引用(也就是左值引用,即 T&)时,绑定规则告诉我们,只能传递给它一个左值(如一个变量,或一个返回引用类型的表达式)。实参可以是const类型,也可以不是。

template <typename T> void f1(T&);  // 实参必须是一个左值

// 对f1的调用使用实参所引用的类型作为模板参数类型
f1(i);      // i 是一个int,模板参数 T 是 int
f1(ci);     // ci 是一个 const int, 模板参数T是 const int
f1(5);      // 错误,5是一个右值

如果一个函数的参数类型是 const T& , 那么正常的绑定规则告诉我们可以传递给它任何类型的实参,比如一个对象(const或者非const),一个临时对象或者是一个字面常量。当参数本身是一个const时,T的类型推断的结果不会是一个const类型。const已经是参数类型的一部分,因此,它不会也是模板参数的一部分。

template<typename T> void f2(const T&);     // 可以接受一个右值

// f2 中的参数已经是 const&, 那么实参中的const也就无关紧要了
// 在每个调用中,f2的函数参数都被推断为 const int&
f2(i);      // i 是一个int,模板参数 T也是 int
f2(ci);     // ci 是一个 const int,模板参数T 还是 int
f2(5);      // const & 可以绑定右值,此时 T 也是 int

1.2 从右值引用参数推断类型

当一个函数的参数是右值引用(即 T&&)时,正常绑定规则告诉我们可以给它一个右值。当我们这样做时,类型推断过程类似普通左值引用函数参数的推断过程。推断出的T的类型是该右值实参的类型:

template <typename T> void f3(T&&);
f3(43);     // 实参是一个int类型的右值,模板参数 T 是 int

1.3 引用折叠和右值引用参数

假定 i 是一个 int 变量,我们可能觉得像 f3(i) 这样的调用是不合法的。毕竟 i 是一个左值,而通常我们是不能将右值引用绑定到左值上的。但是C++在正常绑定规则之外定义了两个例外规则,允许这种绑定。这两个例外规则正是 std::move 能够工作的基础。

  • 例外规则1:
    当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数(如T&&)时,编译器推断模板类型 T 为实参的左值引用类型。因此,调用f(3)时,推断 T 的类型为 int&, 而非 int。
    此时,T 被推断为 int& , 看起来f3的参数就变成了形如 int& && 的格式,即类型为 int& 的右值引用,而通常我们并不能直接定义一个引用的引用。而这就有了下面的例外规则2。

  • 例外规则2
    如果我们间接创建了一个引用的引用,则这些引用就形成了“折叠”。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。在C++11之后,折叠规则同样扩展到右值引用。只有一种例外情况会折叠成右值引用:即右值引用的右值引用。

    • X& &, X& &&, X&& & 都将折叠成左值引用类型 X&
    • X&& && 将折叠成右值引用类型 X&&

注意,引用折叠只能应用于间接创建的引用的引用,例如类型别名或者模板参数。

在上述规则的加持下,就有了所谓的“万能引用”的说法,万能引用既可以绑定左值,也可以绑定右值,且不论如何都会推导出一个引用类型(左值引用或右值引用)。

万能引用的形式:

template <typename T>
void f3(T&& t);             // t 是万能引用

或者
int get_value(){ return 3; }
auto &&y = get_value();     // y 是万能引用

右值引用通常用于两种情况, 模板转发其实参,或模板被重载。

常用的右值引用的函数模板通常就是下列形式:

tempalte<typename T> void f(T&&);       // 绑定到非const右值
template<typename T> void f(const T&);  // 左值和const右值

二、完美转发

2.1 普通转发

某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是const以及实参是左值还是右值。

先看一个例子,在这个函数中,它接受一个可调用的表达式和2个额外实参。该函数将调用给定的可调用对象,并将两个额外参数以逆序传递给它。

// flip1 是一个不完整实现,顶层const和引用丢失了
template < typename F, typename T1, typename T2 >
void flip1(F f, T1 t1, T2 t2)
{
    f(t2, t1);
}

void f(int v1, int& v2)
{
    cout << v1 << " : " << ++v2 << endl;
}

很明显,上述flip1被调用时,f函数的引用丢失了,程序执行后不能得到预期的效果。

2.2 定义能保持类型信息的函数参数

我们需要使flip函数的参数能保持给定实参的“左值性”,更进一步,我们也希望保留参数的const属性。

通过将一个函数参数定义为一个指向模板类型参数的右值引用,我们可以保持其对应实参的所有类型信息。而使用引用参数(无论是左值引用还是右值引用),将使得我们可以保留const属性,因为在引用类型中const是底层的。如果我们将参数定义为 T1&& 和 T2&&, 通过引用折叠,就可以保持实参的左值、右值属性了。

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

这个版本的flip2解决了一半问题,它对于接受一个左值引用的函数工作的很好,但不能用于接受右值引用参数的函数,例如:

void g(int&& i, int& j)
{
    cout << i << " : " << j << endl;
}

flip2(g, i, 42);    // 错误,不能从一个左值实例化int&&

2.3 完美转发

那么该如何解决上述问题呢? 我们可以使用一个名为 std::forward 的新的标准库函数来传递flip2的参数,它能保持原始实参的类型。类似于 std::move, 它定义于 utility 头文件中。和 std::move 不同,std::forward 必须通过显式模板实参来调用。 std::forward 返回该显式实参类型的右值引用。 即:
std::forward 的返回类型是 T&& 。

通常情况下,我们使用std::forward 传递那些定义为模板类型参数的右值引用的函数参数。通过其返回类型上的引用折叠,std::forward 可以保持给定参数的左值、右值属性。

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

flip(g, i, 42);

调用flip(g, i, 42)时,i将以 int& 的类型传递给g,42将以 int&& 的类型传递给g。


参考文献:

  1. 《C++ Primer第五版》
  2. 《现代C++语言核心特性解析》

猜你喜欢

转载自blog.csdn.net/hubing_hust/article/details/128651876