完美转发
完美转发是指在模板函数中将接收到的参数无损地转发到另一个函数的能力。这是通过std::forward
来实现的,它是一个特殊的模板函数,用于保持原始参数的值类别(左值或右值)。
当你在模板函数中使用std::forward<Args>(args)...
时,它会根据每个参数的原始值类别来转发这些参数。如果原始参数是左值,那么转发后它仍然是左值;如果原始参数是右值,那么转发后它也是右值。
template<typename... Args>
void wrapper(Args&&... args) {
// 使用std::forward来完美转发参数
another_function(std::forward<Args>(args)...);
}
这种机制非常重要,因为它允许你编写通用代码,同时保持参数的优化(比如避免不必要的拷贝)。完美转发是现代C++中实现高效和灵活代码的关键技术之一。接下来一个一个解释。
万能引用
万能引用,形如T&& args
乍一看这不就是右值引用吗,错啦~,这里还是写完整一点好
template<typename T>
void wrapper(T&& args) {
上边的例子中
右值引用是一个具体的类型才是右值引用,例如 int&& a
,万能引用则类型是由推导可知的
确实看起来像是一个右值引用,但它实际上是一个万能引用。这是因为 T
是一个模板参数,而不是一个具体的类型。当模板函数或方法中出现 T&&
时,其中的 T
是通过类型推导得到的,所以 T&&
可以根据传入参数的类型变成左值引用或右值引用。 这就是万能引用的作用
在模板类中,如果 pushback
函数被调用并传入一个右值,那么 T
将被推导为非引用类型,使得 T&&
成为一个右值引用。如果传入一个左值,T
将被推导为左值引用类型,使得 T&&
实际上成为一个左值引用。这就是万能引用的特性,它可以绑定到左值和右值。
template<typename T>
void relay(T&& arg) {
process(std::forward<T>(arg)); // 完美转发 arg
}
void process(int& x) {
// 处理左值
}
void process(int&& x) {
// 处理右值
}
当你调用 relay(a);
并且 a
是一个左值时,T
推导为 int&
,使得 T&&
实际上变成了 int& &&
。根据引用折叠规则,这最终会折叠成 int&
,所以 arg
是一个左值引用。std::forward<T>(arg)
此时会按照左值处理 arg
。
当你调用 relay(10);
并且 10
是一个右值时,T
推导为 int
,使得 T&&
实际上是 int&&
,一个真正的右值引用。std::forward<T>(arg)
此时会按照右值处理 arg
。
这就是为什么 T&& arg
不总是右值引用,它取决于传入参数的类型。这种机制允许 relay
函数完美转发它的参数,无论它们是左值还是右值。
实际上在移动语义中,也是一样的,如果类型是模板类型则会自动推导,但是传参进来的不管是左值还是右值都无所谓,因为有std::move
那么什么是引用折叠呢?
引用折叠
引用折叠是C++中的一个规则,它处理在模板和类型推导中出现的多层引用的情况。在C++中,你不能直接创建引用的引用,比如 int&& &
或 int& &&
。但在模板和自动类型推导中,这样的情况是可能发生的。引用折叠规则决定了这些多层引用如何被解析为单一的引用。
引用折叠的规则很简单:
- 如果在任何引用对中至少有一个是左值引用(
&
),那么结果就是左值引用(&
)。 - 如果两个引用都是右值引用(
&&
),那么结果就是右值引用(&&
)。
例如
int& &&
折叠为int&
int&& &
折叠为int&
int& &
折叠为int&
int&& &&
折叠为int&&
std::forward
简洁补一下forward
std::forward
是 C++11 引入的一个模板函数,它用于实现所谓的完美转发。完美转发允许你将函数的参数以原始的左值或右值形式传递给其他函数,这意味着保持参数的原始值类别(lvalue 或 rvalue)不变。
当我们定义一个模板函数用于转发参数时,通常我们会希望保持原始参数的左值/右值性质,并且将其转发给另一个函数。假设我们定义如下函数模板:
template <typename T>
void wrapper(T&& arg) {
another_function(arg); // 这里的arg永远是左值
}
这里的 T&&
是通用引用,它可以匹配任意左值或右值。但是问题在于,传给 another_function
的 arg
无论原始参数是左值还是右值,在 wrapper
函数内部,arg
始终是左值。这就是我们需要 std::forward
的地方。
std::forward
的作用就是,保持原有参数的左值/右值性质,并正确地转发给另一个函数。所以上述代码应当修改为:
template <typename T>
void wrapper(T&& arg) {
another_function(std::forward<T>(arg));
}
现在无论 arg
是左值还是右值,std::forward<T>(arg)
都能保持其原有性质,并正确地转发给 another_function
函数。这就是所谓的"完美转发"。
总结一下,std::forward
是C++中实现"完美转发"的重要工具,它能够保持函数参数的原有性质(包括左值、右值特性),并且将其转发给另一个函数。
完美转发==
万能引用可以保持传入参数的属性不变,乍一看很鸡肋,刚才的例子中直接在main中调用process不也就直接调用了对应的左值右值版本的函数了吗,为什么还要调用 relay这个万能引用版本呢?
回想起之前学的可变参数模板就派上用场了
template<typename...Args>
T& emplaceback(Args&&...args)
{
if (m_Size >= m_Capacity)
ReAlloca(m_Capacity + m_Capacity / 2);
new(&m_Data[m_Size]) T(std::forward<Args>(args)...);
//展开: std::forward<Args>(args1),std::forward<Args>(args2),...
return m_Data[m_Size++];
}
完美转发 = 万能引用 + std::forward
第六行placement new语法new(memory) Type(Args),此处意为不使用new分配内存而是直接在m_Data[m_Size](m_Data最后一个数据位置+1)处就地构造一个T类型的参数,参数是万能引用传过来的参数包args,参数包的可变数量可变类型的参数正好合适用完美转发来处理
总之在我看来,完美转发就是通过万能引用来接受传入的参数并根据引用折叠规则来转换成想要的类型后,通过std::forward 正确的转发出去