重温 std::move 以及 std::forward

其实本来是另外一篇 curry 函数的一部分,但是和主题没什么关系,就拆出来了。

std::move

1
2
3
4
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

std::move 做的事就是把类型 static_cast 成对应的右值类型。

通常我们希望 T a = std::move(b) 调用了 Tmove 构造函数,但是如果 b 是带有 const 修饰符的,那么 std::move 的结果的类型就会变成 const T&&,于是调用的就是 copy 构造函数了(const T&& 无法转换成 T&&,但是可以转换成 const T&)。

std::forward

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }

template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}

由于 std::forward 通常用在 universal reference 中,比如下面的代码:

1
2
3
4
5
6
7
void f(const std::string&);
void f(std::string&&);

template<typename T>
void call_f(T&& arg) {
f(std::forward<T>(arg);
}

假如 arg的类型 T&&

  • 右值引用,如 string&&,那么 T 就是 stringstd::forward 的返回类型就是 string&&
  • 左值引用,如 string& ,那么 T 就是 string&std::forward 的返回类型是 string&。(由于引用折叠 reference collapsing 的存在,导致 string& && = string&。)

可见 std::forward 就是把类型 static_cast 成作为函数参数传入时的类型。也许你会问,这不是啥也没干吗?其实对于传入右值引用的情况有所变化,它在函数内用起来就像是左值引用(因为任何有名字的变量必定是左值),有一个简单的例子。

1
2
3
4
5
6
void f(std::string&& x) {
auto y = x; // no move constructor but copy constructor called
std::cout << x << std::endl; // output 123, x is still there
}

f("123");

根据 std::forward 的源代码,可以看到它用 static_assert 防止了一种转换,就是 T 为左值引用的情况,此时返回值也是左值引用,但是参数是右值。显然,把右值 cast 成左值是不合法的,因为会导致 dangling reference,比如:

1
int& x = std::forward<int&>(1);

当然,如果用通常的方法使用,也就是配合模板或者 auto + decltype(...),就肯定不会出现这种异常的转换。