..

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 可以保证传入参数的左值右值属性不会丢失,提高代码的灵活性。

© 2021-2025 powered by hugo and nostyleplease.