为什么需要完美转发

  1. template<typename T>
  2. void g(T x) {
  3. // do something
  4. f(x);
  5. }
  6. int main() {
  7. int a = 42;
  8. g(std::move(a));
  9. g(a);
  10. }

两种不同的 g 函数调用中 f 调用所选择的重载版本都会是左值引用的版本。 实际使用中这种情况非常常见,在 g 函数中检查参数是否正确然后传递给下一个函数调用。

类型推导

template<typename T> void f(T &p); 当一个函数参数是模板类型的普通右值引用时(T&),规定只可以传递一个左值,实参可以是 const 类型。如果实参是 const 类型,那么 T 被推论为 const 类型。

  1. int i = 1;
  2. f(i); // T 被推论为 int
  3. const int ci = 1;
  4. f(ci); // T 被推论为 const int
  5. f(1); // error 传递给 T& 的必须是个左值

template<typename T> void f(const T &p);,由于右值可以绑定 const 左值引用,所以 const T & 可以接受左值。

  1. int i = 1;
  2. f(i); // T 被推论为 int
  3. const int ci = 1;
  4. f(ci); // T 被推论为 int
  5. f(1); // T 被推论为 int

右值引用函数参数类型推导

template<typename T> void f(T &&p); 正常的绑定规则告诉我们应该传递右值f(1);此时类似于左值函数参数推导,模板参数 T 为 int 。

但是f(i);也是被允许的,即使 i 是应该左值,C++ 在正常规则外定义了两个例外规则,允许了这个绑定,完成了类型推导,使得含有 T&& 模板参数的函数在定义中保留了参数的类型信息。

  • 当 f 接受实参是左值是,T 被推论为左值引用类型(int&),f 实例化为 **void f<int&>(int &)**
  • 当 f 接受实参是右值是,T 被推论为左值类型 (int),f 实例化为 **void f<int>(int &&)**

由于 T&& 接受了两种类型的参数,并且内部做出区分等于我们使用模板参数 T 保留了实参的类型信息。所以我们可以使用 T 来进一步处理类型信息(std::foward)。

引用折叠

为了解释上述的逻辑,还引入了引用折叠的观点来解释,引用折叠只会在模板类型参数间接定义,不可以直接使用。( f(T&&) 接受实参是左值时,T&& T 为 T& 整体才为左值)

  1. TR R
  2. T& & -> T& // lvalue reference to cv TR -> lvalue reference to T
  3. T& && -> T& // rvalue reference to cv TR -> TR (lvalue reference to T)
  4. T&& & -> T& // lvalue reference to cv TR -> lvalue reference to T
  5. T&& && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)
  6. ------------------------------------------------------------------------------
  7. template <typename T>
  8. void deduce(T&& x);
  9. int i;
  10. deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
  11. deduce(1); // deduce<int>(int&&)

为什么引入右值引用函数参数类型推导?

假如我们去掉可以 T&& 接受左值的这个特性,然后去实现 g 和 f 的等价调用。

最简单的方法是使用一个 lvalue-reference:

  1. template <typename A, typename B, typename C>
  2. void f(A& a, B& b, C& c)
  3. {
  4. E(a, b, c);
  5. }

f(1, 2, 3); 会报错,因为 A& 无法处理右值

  1. template <typename A, typename B, typename C>
  2. void f(const A& a, const B& b, const C& c)
  3. {
  4. E(a, b, c); // 假如 E 不是 int& 那么也会报错
  5. }

最终解决方案就是在g和f中处理所有情况

  1. template <typename A, typename B, typename C>
  2. void f(A& a, B& b, C& c);
  3. template <typename A, typename B, typename C>
  4. void f(const A& a, B& b, C& c);
  5. template <typename A, typename B, typename C>
  6. void f(A& a, const B& b, C& c);
  7. template <typename A, typename B, typename C>
  8. void f(A& a, B& b, const C& c);
  9. template <typename A, typename B, typename C>
  10. void f(const A& a, const B& b, C& c);
  11. template <typename A, typename B, typename C>
  12. void f(const A& a, B& b, const C& c);
  13. template <typename A, typename B, typename C>
  14. void f(A& a, const B& b, const C& c);
  15. template <typename A, typename B, typename C>
  16. void f(const A& a, const B& b, const C& c);

但是右值引用这种情况仍然无法处理

std::foward

一旦进入函数内部,参数就可以作为左值传递给任何对象

  1. void foo(int&);
  2. template <typename T>
  3. void deduce(T&& x)
  4. {
  5. foo(x); // fine, foo can refer to x
  6. }
  7. deduce(1); // okay, foo operates on x which has a value of 1

利用引用折叠我们可以使用static_cast<T&&>(x); 上面说到 T&& 会保留类型参数,左值-int&,右值-int,再经过 static_cast<T&&>(x); 左值-int&,右值-int&&。

  1. std::forward<A>(a);
  2. // is the same as
  3. static_cast<A&&>(a);