【C++】完美转发

1. 背景

假设我们有一个真正的目标函数 consume,它区分左值/右值:

void consume(int&  x){ std::cout << "左值版本\n"; }
void consume(int&& x){ std::cout << "右值版本\n"; }

现在我们想写一个通用的调用consume的函数 relay,把所有实参原封不动地丢给 consume:

template<class T>
void relay(T arg)          // ① 按值接收
{        
    consume(arg);          // ② 局部变量arg是一个左值!
}

int  a = 1;
relay(a);        // 打印:左值版本  ✅
relay(42);       // 打印:左值版本  ❌(本应右值)

问题就在这:

  • 形参在函数体内永远是左值。这样传入函数的实参不论是左值or右值,形参接过后,在函数体内都作为左值处理。
  • 那么若consume区分左值版本、右值版本,在relay函数体内永远只会调用左值版本,哪怕传入relay的形参的是右值,我们希望调用的也是consume的右值版本。
    而【万能引用+完美转发】的目的就是解决这个问题。

2. std::forward()

(2.1)完美转发

std::forward<T>(arg) 就是根据 T 的原始推导结果 做一次“强制类型转换”:
如果传入左值,则 T = int&,那么std::forward(arg) 返回 int& → 继续当左值。
如果传入右值,则T = int(这里要注意,传入右值时,T被编译器推导为),那么std::forward(arg) 返回 int&& → 还原成右值。

这样在relay函数体内的arg经过forward的“强转”,可以还原为传入relay的形参的实参的类型(左值or右值),从而在relay内按照期望调用对应版本的函数。

传入右值时为什么不是 T = int&&?
C++ 标准规定:
“当形参是 forwarding reference(即 T&& 且 T 是模板参数)时,
如果实参是 左值 int& → T 推导为 int&
如果实参是 右值 int → T 推导为 int(不是 int&&)
然后再做引用折叠。”
这保证了:
T 永远是一个“裸类型”,不带引用;
真正的引用层级(& / &&)由形参的 T&& 和折叠规则自动生成。

(2.2)万能引用

  • “万能引用”(universal / forwarding reference) 必须是 【模板参数】 且 形式恰好为 T&&(或 auto&&)。

一旦写成或因类模板的实例化而导致T变为某种具体类型(如 int&&、std::string&& 等),它就退化为 普通的右值引用,只能绑定右值,不再具备万能引用的“左右值通吃”能力。

template<class T>
void relay(T&& arg) // 这里的T&&不是右值引用,而是万能引用
{      
    consume(std::forward<T>(arg));
}

万能引用退化为普通右值引用的例子(一定要注意!!)

template <class T>
class ThreadStack
{
public:
  bool push(T&& t)
  {
	std::lock_guard lg(this->m_mtx);
	this->m_stk.push(std::forward<T>(t));
	return true;
  }
}

int main()
{
  ThreadStack<int> ts;
  for (int j = 0; j < 5; ++j)
  {
	osy << "thid=" << i << ", j=" << j << "\n";
	ts.push(j);
  }  
}
// ThreadStack<int> 实例化后,签名变成:bool push(int&& t);   //int&&普通右值引用,无法绑定左值j
//报错:“bool ThreadStack<int>::push(T &&)”: 无法将参数 1 从“int”转换为“T &&”

避免这种情况,给函数另一个不同于类模板的模板参数

  template <typename U>
  bool push(U&& t)
  {
	std::lock_guard lg(this->m_mtx);
	this->m_stk.push(std::forward<T>(t));
	return true;
  }
  • 万能引用只有在 模板实参由编译器推导 时才会触发引用折叠规则;一旦你把模板实参显式写出来,它就退化成普通右值引用,左值就绑定不了了。
//错误
//显式指定了int,则U&&变为int&&,j为左值传入报错
template <typename U>
bool push(U&& t)
{
  std::lock_guard lg(this->m_mtx);
  this->m_stk.push(std::forward<T>(t));
  return true;
}

for (int j = 0; j < 5; ++j)
{
	osy << "thid=" << i << ", j=" << j << "\n";
	ts.push<int>(j);
}

(2.3)引用折叠

forward在还原类型时的规则
引用折叠:& && → &,&& && → &&,其余同理。

(2.4)实际使用案例

  • std::vector::emplace_back
template<class... Args>
void vector::emplace_back(Args&&... args)
{
    new (ptr) T(std::forward<Args>(args)...);
}

这个函数可以通过传入构造元素的参数,在vector就地构造,而不需先构造再拷贝。
但也可以传入右值此时,在emplace_back内部是调用元素类型的形参为右值的移动构造。
如:

vector<string> vec;
vec.emplace_back(std::string("hello")); 
//string s("hello"); 
//vec.emplace_back(move(s));

而下面emplace_back则是在内部调用形参为左值的拷贝构造:

vector<string> vec;
string s("hello"); 
vec.emplace_back(s); 
posted @ 2025-08-06 13:38  仰望星河Leon  阅读(57)  评论(0)    收藏  举报