Catalog
  1. 1. 引用折叠
  2. 2. std::move
    1. 2.1. std::move是如何定义的(VS2017)
    2. 2.2. std::move是如何定义的(2)
    3. 2.3. std::move是如何工作的
  3. 3. 转发
  4. 4. 完美转发 std::forward
C++ | 完美转发和引用折叠

参考书籍《C++ Primer 5th》

引用折叠

对于函数

1
2
3
4
template <typename T> void f3(T &&);
//实参是一个int类型的右值;模板参数T是int类型
//相当于实例化了一个void f3<int> (int &&)的函数
f3(42);

假定i是一个int对象,当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数,编译器会推断模板类型参数是”实参的左值引用类型”。因此,当我们调用f3(int(i))时,编译器会推断模板参数T的类型为int&,而非int

T被推断为int&看起来好像意味着f3的函数参数应该是一个类型int&的右值引用

int& &&

一般来说,我们不能直接定义一个引用的引用。

但是,通过类型别名或通过模板类型参数间接定义是可以的

在这种情况下,

如果我们间接创建了一个引用的引用,则这些引用形成了”折叠”,这就是引用折叠

引用折叠有两个规则:

  • X& &X& &&X&& &都折叠成类型X&(左值引用)
  • X&& &&会被折叠成X&&(右值引用)

引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数

当一个模板参数T被推断为引用类型时,则T&&被折叠为一个左值引用的类型。

例如,

1
2
3
4
5
template <typename T> void f3(T&&);
int i;
f3(i);
//无效代码,仅用于演示
//void f3<int&>(int& &&);

f3的函数参数是T&Tint&,因此T&&int& &&,根据规则会被折叠成int &。因此,及时f3的函数形式参数是一个右值引用(T&&),此调用也会用一个左值引用类型实例化f3

void f3 <int&> (int &)

这两个规则导致了两个重要结果:

  1. 如果一个函数参数是一个指向模板类型参数的右值引用(如,T&&),则它可以被绑定到一个左值
  2. 如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将实例化为一个左值引用参数(T&)

这两条规则暗示,我们可以将任意类型的实参传递给T&&类型的函数参数。

std::move

虽然不能将右值引用绑定到一个左值上,但可以用std::move来获取一个左值上的右值引用。

这是一个引用折叠很好的例子

std::move是如何定义的(VS2017)

1
2
3
4
5
6
template<class _Ty>
constexpr remove_reference_t<_Ty>&&
move(_Ty&& _Arg) _NOEXCEPT
{ // forward _Arg as movable
return (static_cast<remove_reference_t<_Ty> &&>(_Arg));
}

remove_reference中有一个名为type的类型成员。如果我们用一个引用类型实例化remove_refernce,则type表示被引用的类型,若remove_reference<int&>type成员将是int

其实看到这里的时候我很好奇remove_reference_t又是怎样定义的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<class _Ty>
//原模板参数
struct remove_reference
{ // remove reference
using type = _Ty;
};

template<class _Ty>
struct remove_reference<_Ty&>
{ // remove reference
using type = _Ty;
};

template<class _Ty>
struct remove_reference<_Ty&&>
{ // remove rvalue reference
using type = _Ty;
};

template<class _Ty>
using remove_reference_t = typename remove_reference<_Ty>::type;

remove_reference的实现是通过一个叫做模板部分特例化(partial specialization)的机制来实现的。

我会在后面来填坑

PS:实际研读一下源码觉得C++真的是花里胡哨的

继续回到std::move

std::move是如何定义的(2)

std::move是引用折叠的一个很好的例子

1
2
3
4
5
template <typename T>
typename remove_reference<T>::type &&move(T &&t)
{
return static_cast<typename remove_reference<T>::type &&> (t);
}

首先,move的函数参数T&&是一个指向模板类型参数的右值引用。

通过引用折叠,此参数可以与任何类型的实参匹配。特别是,我们既可以传递给move一个左值,也可以给它传递一个右值。

1
2
3
std::string s1("hi!"), s2;
s2 = std::move(std::string("bye!")); //从一个右值一定数据
s2 = std::move(s1); //从一个左值移动数据,移动之后s1的值是不确定的

std::move是如何工作的

在上述第一个赋值中,传递给move的实参是string的构造函数的右值结果。由实参推断出来的类型为被引用的类型。

因此,在std::move(std::string("bye!"))中:

  • 推断出T的类型为std::string
  • 因此,remove_referencestd::string进行实例化
  • remove_reference<std::string>type成员是std::string
  • move的返回类型是std::string&&
  • move的函数参数t的类型为std::string&&

因此,这个调用实例化std::move<std::string>,即函数

std::string &&move(std::string &&t)

函数体返回static_cast<std::string>(t)t的类型已经是std::string&&,于是类型转换什么也不做。

因此,此调用的结果就是它接受的右值引用。

对于第二个赋值,在std::move(s1)中:

  • 推断出T的类型为std::string&(std::string的引用,而非普通的std::string)
  • 因此,remove_referencestd::string&进行实例化
  • remove_reference<std::string&>type成员是std::string
  • std::move的返回类型任然是std::string&&
  • std::move的函数参数实例化为std::string& &&会折叠为std::string&

因此,这个调用实例化std::move<std::string&>,即

std::string &&move(std::string &t)

在此情况下,t的类型为std::string &static_cast将其转换为std::string &&

PS: 右值转瞬即逝

转发

某些函数需要将一个和多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发类型的所有性质,包括实参类型是否非const以及实参是左值还是右值。

例如,

1
2
3
4
5
template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
f(t2, t1);
}

当我们希望用它调用一个接受引用参数的函数时会出现问题

1
2
3
4
void f(int v1, int &v2)
{
std::cout << v1 << " " << ++v2 << std::endl;
}

在这段代码中,f改变了绑定到v2的实参的值。但是,因为我们通过flip1调用ff所做的改变就不会影响实参

1
2
f(42, i);
flip1(f, j, 42);

问题在于j被传递给flip1的参数t1是一个普通的,非引用的类型int,所以会导致j的值是被拷贝t1中。f中的引用被绑定到t1而非j

完美转发 std::forward

通过一个名为forward的新标准库设施可以保持原始的实参类型。和move一样,forward定义在头文件utility中。

forward必须通过显示模板实参调用。forward返回该显式实参类型的右值引用。即,forward<T>的返回类型是T&&

通常情况下,我们使用forward传递那些定义为模板类型参数的右值引用的函数参数。通过其返回类型上的引用折叠,forward可以保持给定实参的左值/右值属性:

1
2
3
4
5
template <typename Type> intermediary(Type &&arg)
{
finalFcn(std::forward<Type>(arg));
// ...
}

本例中,我们使用Type作为forward的显式模板实参类型,它是从arg推断出来的。

由于arg是一个模板类型参数的右值引用,Type将表示传递给arg的实参的所有类型信息。

  • 如果实参是一个右值,则Type是一个普通类型,forward<Type>将返回Type&&
  • 如果实参是一个左值,则通过引用折叠,Type本身是一个左值引用类型。再此情况下,此返回类型是一个指向左值引用类型的右值引用。再次对forward<Type>的返回类型进行引用折叠,将返回一个左值引用类型。

通过完美转发,我们可以重写flip函数

1
2
3
4
5
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2)
{
f(std::forward<T2>(t2), std::forwad<T1>(t1));
}

如果我们调用

1
2
3
int g;
int i;
flip(g, i, 42);

此时,i将以int&类型传递给g42将以int &&类型传递给g

Author: Jsthcit
Link: http://jsthcitpizifly.com/2020/12/18/perfect-forward/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
  • 支付寶