围绕std::move()谈谈通用引用,类型推导和引用折叠

从std::move()说起
对于std::move(),既可以接受一个左值,也可以接受一个右值。它的函数定义是这个样子的
template <typename T>
typename remove_reference<T>::type&& move(T&& t) noexcept {
return static_cast<typename remove_reference<T>::type&&>(t);
}
但形参中的T&&并不是是一个右值引用, 如果他是一个右值引用, 那么下面代码是无法编译通过的
Foo f;
std::move(f); // error
其实这里的T&&是一个通用引用(universal reference)。
通用引用
T&& Doesn’t Always Mean “Rvalue Reference” by Scott Meyers
通用引用有一条性质,当匹配一个左值表达式时, 会表现的像一个左值引用; 当匹配一个右值表达式的时候, 会表现的像一个右值引用. (后面会谈到是如何实现的)
构成通用引用的条件
正如 Scott Meyers
If a variable or parameter is declared to have type T&& for some deduced type T, that variable or parameter is a universal reference.
所说的, 构成通用引用必须具有两个条件
- 形如
T&& - T的类型需要推导(deduced type)
举例
- 必须形如
T&&```cpp int lvalue = 3;
const auto&& r = lvalue; //错误:r不是通用引用,而是右值引用,故不能通过左值初始化!
template void func(const T&& p);
func(lvalue); //错误:p不是通用引用,而是右值引用,故不能通过左值初始化!
必须是`T&&`, 哪怕是`const T &&`都不行。
* 必须存在类型推导
```cpp
template<typename T>
class Bar {
private:
T member;
public:
Bar(Bar&& other); // 不是通用引用,而是右值引用
};
other的类型是Bar&&, 而非T&&. 因此,是右值引用。
类型推导和引用折叠
那么通用引用是如何实现 当匹配一个左值表达式时, 会表现的像一个左值引用; 当匹配一个右值表达式的时候, 会表现的像一个右值引用 的呢?
- 编译器允许它匹配左值和右值
- 类型推导和引用折叠最终让它变成
左值引用或右值引用
类型推导规则
通用引用auto&&和T&&的类型推导规则是:
对于
template<typename T>
void f(T&& param);
f(expr);
// auto 和 模版 通过一个非常规范非常系统化的转换流程来转换彼此
若匹配的expr是
左值(lvalue),auto或T被推导成lvalue的引用类型;例如匹配i时(int i=3;),推导结果是int&;这非常不寻常,这是模板类型推导中唯一一种T被推导为引用的情况若匹配的expr是
右值rvalue,auto或T被正常推导成rvalue的类型;例如匹配3时,推导结果是int;
类型推导带来的问题
在C++中,引用的引用是非法的,如果在编写代码是写出引用的引用,编译器会报错。
int f = 3;
int & & r1 = f; //error: cannot declare reference to ‘int&’, which is not a typedef or a template type argument
int & && r2 = f; //error: cannot declare reference to ‘int&’, which is not a typedef or a template type argument
int && & r3 = f; //error: cannot declare reference to ‘int&&’, which is not a typedef or a template type argument
int && && r4 = f; //error: cannot declare reference to ‘int&&’, which is not a typedef or a template type argument
但是在编译期,根据类型推导的规则,就会产生引用的引用
template <typename T>
void func(T&& x) {} // 等价于 void func(int& && x);
int main() {
int a = 0;
func(a);
}
由于T&& 接受一个左值a, 因此T被类型推导为int&, 此时函数定义等价于 void func(int& && x);
由于引用的引用是非法的。那我们这些例子中都存在引用的引用怎么办呢?好在,这些引用的引用不是存在于源代码中(否则编译失败),而是在编译过程中临时产生的。编译器会立即消除它们,手段就是引用折叠(reference collapsing)。
引用折叠
引用折叠规则: “如果任一引用为左值引用,则结果为左值引用。否则(即,如果引用都是右值引用),结果为右值引用。”
也就是说:
- 右值引用的右值引用折叠成(collapses into)右值引用;
- 其他情况折叠成(collapses into)左值引用;
这样上面的void func(int& && x); 被折叠成为 void func(int& x); 顺利通过编译。
类型推导和引用折叠在通用引用机制中起着重要作用:编译器允许T&&匹配左值和右值,然后经过类型推导和(或)引用折叠,T&&才最终变成左值引用或者右值引用。
回到std::move()
那么std::move()是怎样的工作的呢?
template <typename T>
typename remove_reference<T>::type&& move(T&& t) noexcept {
return static_cast<typename remove_reference<T>::type&&>(t);
}
typename的作用是在remove_reference<T>::type前使用了typename关键字来修饰,编译器才会将该名称当成是类型, 具体可参考知无涯之C++ typename的起源与用法。
remove_reference的作用是去掉类型的引用(左值引用和右值引用), 直接返回类型的基本内容。
// 对于 move1(Foo())
// Foo()是右值, T的类型被推断为Foo, move函数被推断为
Foo&& move1(Foo&& item) noexcept {
return static_cast<Foo&&>(item);
}
// 对于 f=Foo(); move1(f);
// f是左值, T的类型被推断为 Foo&
//Foo&& move1(Foo& && item) noexcept {
// return static_cast<Foo&&>(item);
//}
// 又因为引用折叠, 被推断为
Foo&& move1(Foo& item) noexcept {
return static_cast<Foo&&>(item);
}
因此std::move()只是一个语法糖, 并不会带来运行时开销。

Ref
- https://www.yuanguohuo.com/2018/05/25/cpp11-universal-ref/




