参考书籍《C++ Primer 5th》
引用折叠
对于函数
1 | template <typename T> void f3(T &&); |
假定i
是一个int
对象,当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数,编译器会推断模板类型参数是”实参的左值引用类型”。因此,当我们调用f3(int(i))
时,编译器会推断模板参数T的类型为int&
,而非int
T
被推断为int&
看起来好像意味着f3
的函数参数应该是一个类型int&
的右值引用
int& &&
一般来说,我们不能直接定义一个引用的引用。
但是,通过类型别名或通过模板类型参数间接定义是可以的
在这种情况下,
如果我们间接创建了一个引用的引用,则这些引用形成了”折叠”,这就是引用折叠。
引用折叠有两个规则:
X& &
、X& &&
和X&& &
都折叠成类型X&
(左值引用)X&& &&
会被折叠成X&&
(右值引用)
引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数
当一个模板参数T
被推断为引用类型时,则T&&
被折叠为一个左值引用的类型。
例如,
1 | template <typename T> void f3(T&&); |
f3
的函数参数是T&
且T
是int&
,因此T&&
是int& &&
,根据规则会被折叠成int &
。因此,及时f3
的函数形式参数是一个右值引用(T&&
),此调用也会用一个左值引用类型实例化f3
void f3 <int&> (int &)
这两个规则导致了两个重要结果:
- 如果一个函数参数是一个指向模板类型参数的右值引用(如,T&&),则它可以被绑定到一个左值
- 如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将实例化为一个左值引用参数(
T&
)
这两条规则暗示,我们可以将任意类型的实参传递给T&&
类型的函数参数。
std::move
虽然不能将右值引用绑定到一个左值上,但可以用std::move
来获取一个左值上的右值引用。
这是一个引用折叠很好的例子
std::move是如何定义的(VS2017)
1 | template<class _Ty> |
remove_reference
中有一个名为type
的类型成员。如果我们用一个引用类型实例化remove_refernce
,则type
表示被引用的类型,若remove_reference<int&>
则type
成员将是int
其实看到这里的时候我很好奇remove_reference_t
又是怎样定义的
1 | template<class _Ty> |
remove_reference的实现是通过一个叫做模板部分特例化(partial specialization)的机制来实现的。
我会在后面来填坑
PS:实际研读一下源码觉得C++真的是花里胡哨的
继续回到std::move
std::move是如何定义的(2)
std::move
是引用折叠的一个很好的例子
1 | template <typename T> |
首先,move的函数参数T&&
是一个指向模板类型参数的右值引用。
通过引用折叠,此参数可以与任何类型的实参匹配。特别是,我们既可以传递给move
一个左值,也可以给它传递一个右值。
1 | std::string s1("hi!"), s2; |
std::move是如何工作的
在上述第一个赋值中,传递给move
的实参是string
的构造函数的右值结果。由实参推断出来的类型为被引用的类型。
因此,在std::move(std::string("bye!"))
中:
- 推断出
T
的类型为std::string
- 因此,
remove_reference
用std::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_reference
用std::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 | template <typename F, typename T1, typename T2> |
当我们希望用它调用一个接受引用参数的函数时会出现问题
1 | void f(int v1, int &v2) |
在这段代码中,f
改变了绑定到v2
的实参的值。但是,因为我们通过flip1
调用f
,f
所做的改变就不会影响实参
1 | f(42, i); |
问题在于j
被传递给flip1
的参数t1
是一个普通的,非引用的类型int
,所以会导致j
的值是被拷贝到t1
中。f
中的引用被绑定到t1
而非j
。
完美转发 std::forward
通过一个名为forward
的新标准库设施可以保持原始的实参类型。和move
一样,forward
定义在头文件utility
中。
forward
必须通过显示模板实参调用。forward
返回该显式实参类型的右值引用。即,forward<T>
的返回类型是T&&
通常情况下,我们使用forward
传递那些定义为模板类型参数的右值引用的函数参数。通过其返回类型上的引用折叠,forward
可以保持给定实参的左值/右值属性:
1 | template <typename Type> intermediary(Type &&arg) |
本例中,我们使用Type
作为forward
的显式模板实参类型,它是从arg
推断出来的。
由于arg
是一个模板类型参数的右值引用,Type
将表示传递给arg
的实参的所有类型信息。
- 如果实参是一个右值,则
Type
是一个普通类型,forward<Type>
将返回Type&&
- 如果实参是一个左值,则通过引用折叠,
Type
本身是一个左值引用类型。再此情况下,此返回类型是一个指向左值引用类型的右值引用。再次对forward<Type>
的返回类型进行引用折叠,将返回一个左值引用类型。
通过完美转发,我们可以重写flip
函数
1 | template <typename F, typename T1, typename T2> |
如果我们调用
1 | int g; |
此时,i
将以int&
类型传递给g
,42
将以int &&
类型传递给g