在解释任何东西以前 我都必须要强调 我们为什么需要这个东西 如果一个东西我们都是不需要的 那么我们解释他干嘛? 假定你彻底了解了一个东西 但是你并不知道你为什么需要他 他能解决什么问题 那你仅仅就只是背了一段理论性的东西 对于你本人的成长毫无用处 这里我们一次性讲懂万能引用 引用折叠以及完美转发三个知识点 如果任意一个知识点没看懂 请停下来好好看 切勿囫囵吞枣 不然你绝对是一知半解
网上一些文章属实误人子弟 看了让人恶心
前置知识:
我假设你已经懂了最基本的概念 如左值 右值 移动语义 值类型和值类别区别 我强烈建议你先搞懂这些概念再读本文 如果分不清左值右值 这里贴出c++11标准文档原文来帮助你分清 别去搞啥xvalue 啥将亡值 你要是脑子还是正常的 那么以下三条规则就足够你分清左值和右值直到你入土
先来搞清楚 标准文档原文中的expression到底是啥 这里贴出cppreference的定义
An expression is a sequence of operators and their operands, that specifies a computation.
这句话翻译成人话就是 表达式是一系列操作数和操作的集合 这个集合明确了计算过程
但是我这里要强调 操作可以是0个 但是操作数必须是1个及其以上 如下图
最基本的操作如+-*/ 再比如& [] ->这些都可以被称之为是操作
而操作数这不用多说了吧
int i=4; &i;//&i为表达式 int arr[5]{}; i;//i为表达式 i+4;//i+4为表达式 4;//5为表达式 arr[4];//arr[4]为表达式
- If you can take the address of an expression, the expression is an lvalue.
- 如果你可以对表达式取地址 那么表达式是一个左值
- If the type of an expression is an lvalue reference (e.g.,
T&
orconst T&
, etc.), that expression is an lvalue.- 如果表达式是一个左值引用,那么表达式是一个左值(但是这里注意啊 我这里自己给你们说 即便是右值引用 表达式仍旧是一个左值 因为可以被取地址)
- 作者提示 所有的左值引用都是左值 即便他是作为临时对象 他仍然是左值! 记住这一条 这会影响到我们后续对于完美转发的讲解!(这是c++的规则) 例子如下
int& func(int& i) { cout << "func(int&)" << endl; return i; } void func(int&& i) { cout << "func(int&&)" << endl; } int main() { int i = 4; func(func(i));//func(i)的返回值为左值! 我再次提示 }
- Otherwise, the expression is an rvalue. Conceptually (and typically also in fact), rvalues correspond to temporary objects, such as those returned from functions or created through implicit type conversions. Most literal values (e.g.,
10
and5.3
) are also rvalues.- 除以上两条规则之外 表达式是一个右值 从概念上以及理论上都应是如此 比如从函数返回值返回的临时对象和隐式转换得来的临时对象以及大多数的字面量(1,3)
好 更多的前置知识我已经不想再去讲解了 下面让我们来看看为什么我们需要万能引用(下面是作者自己的见解 如果有问题还请多见谅 因为我找了很多资料也确实没发现有什么地方必须要使用到万能引用才能解决问题的,他更多的是和模板类型自动推导来一起来最大程序复用代码的,方便大家编程)
大家都知道c++模板的意义是为了尽最大可能的去复用代码简化大家的使用 那么我们按照c++11以前的方式要去写一个既能接受右值引用 又要接受左值引用的模板 那么我们就不得行写两个版本了 两个版本书写方式如下
template<typename T>
void test(T& t)//左值引用版
{
t++;
return;
}
template<typename T>
void test(const T& t)//右值引用版
{
t++;
return;
}
那么在c++11就有没有办法能够直接一次性把两个全部搞定呢? 让编译器来自动给我们推导我们需要的类型是啥 而不是我们手动还要去写两遍模板 c++11便推出了万能引用 万能引用的方式如下
特别重要 特别重要 特别重要
标准文档原文:
If a variable or parameter is declared to have type
T&&
for some deduced typeT
, that variable or parameter is a universal reference.作者翻译之后说人话就是 当且仅当T&& T这个东西是模板参数的时候 并且一定要涉及到编译器模板类型自动推导!!!! 他才表示万能引用 不然他是个右值引用啊
int&& i=4;//int&&为右值引用
template<typename T>
void test(T&& t);//T&&为万能引用
auto&& i=4;//涉及到编译器模板类型自动推导 并且有&& auto&&为万能引用
我们改造我们的函数为万能引用版
template<typename T>
void test(T&& t)//万能引用版
{
t++;
return;
}
现在我们的test函数就既可以接受左值 也可以接受右值了
好了 到这里为止 我们对于万能引用的介绍就已经做完了 那么完美转发和引用折叠又是什么鬼玩意呢? 既然有这两个东西 说明万能引用肯定是遇到了一些问题的 不然不可能要推出完美转发和引用折叠 我们来看看他遇到了啥问题
如下图所示 4为右值 我们本来想在模板里面去调用真正正确的函数 但是却调用到了左值引用的版本 这明显不是我们想要的结果 造成这个问题的最本质原因是啥呢?
超级重要 超级重要 超级重要
当我们传递4给test函数的时候 发生了模板类型自动推导 然后
T的类型被推断为了int T的类型被推断为了int T的类型被推断为了int T的类型被推断为了int
t的类型被推断为了int&& t的类型被推断为了int&& t的类型被推断为了int&&
T和t不是同一个类型 T和t不是同一个类型 T和t不是同一个类型 T和t不是同一个类型
因为非常重要 所以我不得不多强调几次 这里涉及到模板类型推导规则 这不是本文的重点 作者再次强调 至于他为什么要被推导为这个类型 详情可以参阅c++标准文档中对于模板类型推导规则的概述 总之你就记住 他被人为的规定为了强制推导为上述类型
那么问题就来了 当t的类型被推断为int&&的时候 我们都已经在上文说过了 只要能被取地址 那么他就是一个左值 这里明显右值引用是能够被取地址的 那么他就变成了一个左值 左值肯定就调用到func(int&)版本去了呗
那么当我们需要维持原来的值类别的时候 我们就需要讲我们的代码改为完美转发版本 如下所示
我们加上forward<T>(t) 表示完美转发 而完美转发的实现原理要由引用折叠来支撑
上文我们看到了 完美转发可以解决我们的问题 但是我们不禁好奇 他到底是怎样做到的呢?很简单 作者带你看源码forward的源码 打上断点 我们可以看到我们实际调用的forward版本
上文我们已经说到了T被推断为了int类型 那么在使用forward<T>时相当于是 forward<int> 那么下图中的_Ty就是int
我们可以看到返回值为_Ty&& 返回值也是一个万能引用 OK 对于返回值的讲解就完成了
我们看到参数为 remove_reference_t<_Ty>& 就相当于remove_reference_t<int>& 那么我们不禁好奇 这个remove啥的又是啥 我们继续进去看
可以看到remove_referfence_t<int>就等于 remove_reference<int> 然后我们再往上找他的特化版本
那么就很容易看出他的作用了 就如他的名字一般 可以擦除引用类型拿到实际类型 比如remove_reference_t<int&&>这个类型就相当于是int现在我们可以继续回到forward版本了
经过上面的推导 那么这里的参数就变为了int& _Arg 那么不就是我们想要的左值引用版本嘛? 我们传入的是forward<T>(t) t是右值引用 但是本身是左值 这不就正好传递进去了嘛
对于参数的讲解就到这里了 这没有任何问题了
接下来最匪夷所思的一幕来了 可以看到 函数体仅仅只做了一个操作
static_cast<_Ty&&>
也就是说他仅仅只是给咋们的这个T类型加上了两个&& 那么返回值就为int&& 这刚好就是一个右值引用 并且我们也说过 函数的返回值除左值引用外均是右值 这不就完美的保留了咋们的值类别了嘛? 既传入的是右值 我转发出去以后 仍然是一个右值
对于左值来说 T类型会被推断为int& 经过这个static_cast后就会变成int& && 那么这三个引用号又是怎样转发的呢
接下来就引入了我们的引用折叠规则 在编译器眼中是可以存在两个以上的引用号的 而用户眼中则不可以 这是什么意思呢 例子如下 可以明确看到我们用户不允许使用&&&三个引用号 这里贴上标准文档中的解释
The true core of the issue is that some constructs in C++11 give rise to references to references, and references to references are not permitted in C++. If source code explicitly contains a reference to a reference, the code is invalid:
Widget w1;
Widget& & w2 = w1; // error! No such thing as “reference to reference”
There are cases, however, where references to references arise as a result of type manipulations that take place during compilation, and in such cases, rejecting the code would be problematic. We know this from experience with the initial standard for C++, i.e., C++98/C++03.
作者翻译成人话给你们说就是 用户层不允许使用引用的引用 而编译阶段是允许存在的引用的引用的
所以 int& &&这种类型在编译器经过引用折叠就变成了普通的int& 引用折叠的规则如下:
看到网上很多文章在哪里说 啥int&& &&->int&& 之类的列一大堆引用折叠的规则 但是标准文档说的很清楚了 只有两条引用折叠规则
标准文档中对于引用折叠的描述如下
There are only two reference-collapsing rules:
- An rvalue reference to an rvalue reference becomes (“collapses into”) an rvalue reference.
- All other references to references (i.e., all combinations involving an lvalue reference) collapse into an lvalue reference.
作者翻译成人话就是
只有T&& &&会被折叠为右值引用 其他全部为左值引用 仅此而已
经过我们对于引用折叠的描述 那么int& &&就被折叠为了普通的左值引用 左值引用作为返回值 仍然是左值 那么他就正确的调用了我们想要的版本