Inside of std::forward
std::forward
的实现与通用引用(universal reference) 和 重载决议 紧密相关。
forward 的两个版本
在《C++程序设计语言(第 1~3 部分)》的第 586 页有一个很好的例子:
模板实参推断过程中是区分左值和右值的:X类型的左值会被推断为一个X&,而右值被推断为X。
template <typename T> T&& forward(typename std::remove_reference<T>::type& t) noexcept; template <typename T> T&& forward(typename std::remove_reference<T>::type&& t) noexcept; template <typename TT, typename A> unique_ptr<TT> make_unique(A&& a) { // 这里我稍稍改变了一下 `TT` 类型的的构造函数的参数要求 // 需要一个传入参数 return unique_ptr<TT>{new TT{forward<A>(a)}}; }
这里简单分析一下forward
重载模板函数的重载解析规则,当一个左值(比如 int x{};
)作为 forward的参数,参数推断出的特例化版分别是:
// (int&)&& forward<int&>(std::remove_reference<int&>::type& t) noexcept;
// 经过简化之后
int& forward<int&>(int& t) noexcept;
// (int&)&& forward<int>(std::remove_reference<int>::type&& t) noexcept;
int&& forward<int&>(int&& t) noexcept;
这两个版本特例化程度相当,同时进入函数的匹配候选中。
又因为精确匹配是第一优先级,左值会匹配 int&
的版本,返回一个左值引用。
多提一句:foo(int)
和 foo(int&)
对于传入参数 x
有相同的优先级,所以在编译时会报错。
同样的手法可以推导出一个右值类型的参数会匹配到 int&&
的版本,它返回一个右值引用。
这样一组 forward
模板函数的能力:
-
当实参是一个左值,它的返回值类型为左值引用。
-
当实参是一个右值,它的返回值类型为右值。
乍一看它好像没有什么作用,但一旦放到模板编程里就能看到它的价值了。
利用通用引用
继续分析最后一段代码:
template <typename TT, typename A>
unique_ptr<TT> make_unique(A&& a) {
return unique_ptr<TT>{new TT{forward<A>(a)}};
}
这个函数的功能简单明了:构造一个 unique_ptr<TT>
并返回它。
我们详细分析一下 A 的推断过程。
当传入参数是一个左值类型如 int
,推断结果为:
template <typename TT, int&>
unique_ptr<TT> make_unique(
// make_unique 形参类型为 int&,只能接受一般左值实参
int& a) {
return unique_ptr<TT>{new TT{forward<int&>(a)}};
}
// forward<int&>(a) 的返回值为 int& 类型的值作为 TT 的初始化参数
右值 int&&
版本:
template <typename TT, int>
unique_ptr<TT> make_unique(
// make_unique 形参类型为 int&&,只接受右值作为实参参数
int&& a) {
return unique_ptr<TT>{new TT{forward<int>(a)}};
}
// forward<int>(a) 的返回值为 int&& 类型的值作为 TT 的初始化参数
我们再用不使用 forward
的版本作为对比。
template <typename TT, typename A>
unique_ptr<TT> make_unique(A&& a) {
return unique_ptr<TT>{new TT(a)};
}
// 当传入左值,a 是一个左值;当传入右值,**表达式 a** 是左值,即使 a 的类型是 int&&
关于 a 是左值可以参考《C++ Primer》的第 472 页。
变量是左值
变量可以看作只有一个运算对象而没有运算符的表达式,虽然我们很少这样看待变量。类似其他任何表达式,变量表达式也有左值/右值属性。变量表达式都是左值。带来的结果就是,我们不能将一个右值引用绑定到一个右值属性的变量上。
int&& rval1 = 1; //合法 int&& rval2 = rval1; //不合法,rval1 表达式是一个左值; // 这也是其他形参列表中有右值类型的函数,函数体内的赋值操作需要加 std::move 的原因
这就导致不使用 forward
的版本无论在哪种情况下都会调用 TT
的左值版本的构造函数。
结论
在模板使用 std::forward
可以保证传入参数的左值右值属性不会丢失,提高代码的灵活性。