C++11:类型推导与完美转发复盘笔记

引用折叠

  1. 引用折叠要解决的问题:
    • 用于实现万能引用语法,旨在正确处理左值与右值传参等场景,搭配forward可实现完美转发,降低函数连续调用时的深拷贝开销
    • 具体来说,如调用A(T&& arg),而A中调用B(T& a)/B(T&& a),则由于右值引用为左值类型,所以会调用B(T& a),则到最终使用a时,发现a为左值,若需要对a进行更改等操作,就需要重新深拷贝一份a,这样就会造成:明明最开始传参时使用的是右值,但经历函数调用后却变为了左值,就失去了右值的意义,而使用万能引用 + forward实现的完美转发,就能解决这种问题
  2. 引用折叠语法:
  • template void func(T& arg)
  • template void func(const T& arg)
  • template void func(T&& arg)
  • auto&
  • const auto&
  • auto&&
  • typedef int& it;
  • typedef const int& it;
  • typedef int&& it;
  1. 触发条件:
    在auto/typedef/模板类型推导时,均会触发引用折叠
  2. 引用折叠规则(以模板推导为例):
  • template void func1(T& arg)
    • 传参为T时,T 会推导为 T 类型,折叠后推导为 T&
    • 传参为T&时,T 会推导为 T& 类型,折叠后推导为 T&
    • 传参为T&&时,T 会推导为 T&& 类型,折叠后推导为 T&
    • 传参为右值时,无法推导,传参失败
  • template void func2(const T& arg)
    • 传参为T时,T 会推导为 T 类型,折叠后推导为 const T&
    • 传参为T&时,T 会推导为 T& 类型,折叠后推导为 const T&
    • 传参为T&&时,T 会推导为 T&& 类型,折叠后推导为 const T&
    • 传参为右值时,T 会推导为 T&& 类型,折叠后推导为 const T&
  • template void func3(T&& arg)
    • 传参为T时,T 会推导为 T 类型,折叠后推导为 T&
    • 传参为T&时,T 会推导为 T& 类型,折叠后推导为 T&
    • 传参为T&&时,T 会推导为 T& 类型,折叠后推导为 T&
    • 传参为右值时,T 会推导为 T&& 类型,折叠后推导为 T

简洁总结就是(下面P指的是折叠后的类型):

  1. T作为模板参数来说,T和P无论如何都会推导成不带&与const的类型;
    const T& 作为模板参数时,T也会推导为不带&与const的类型,P一定为const T& 类型
  2. 所以说,特殊的模板推导就是T&&和T&:
  • T&&作为模板参数时,T和P均会保留原const属性,且左值时,T推导为T&,折叠后p类型为T&;右值时,T推导为T,折叠后p类型为T&&(由于int&&为左值类型,所以T被推导为T&,即int&,与&&折叠后结果仍为左值)
  • T&作为模板参数时,仅能接收左值类型,且T和P均会保留原const属性,且T推导为T,折叠后P类型为T&
  1. T推导时,绝对不会被推导为T&&类型,或者说,对于万能引用的T推导,T仅能推导为T或T&两种情况

引用折叠实验代码链接:

模板参数推导实验

类型推导(auto 与 decltype)

C++98引入auto关键字,但此时的auto还不具备自动推导功能,其作用为一个存储类说明符,这里不做展开;C++11引入decltype关键字,且auto可以用于自动类型推导,接下来具体介绍auto与decltype

差异与行为如下:

  1. auto的自动推导行为以及decltype关键字均为C++11引入
  2. auto面对左值/右值时,会自动推导为T类型(不保留const),auto&与auto&&则会按照引用折叠规则进行推导,即auto&&为万能引用行为(左值/左值引用auto推导为T&,右值为T,折叠后为T&与T&&),auto&则将左值推导为T&,右值则禁止推导
  3. decltype关键字有两种形式,decltype(x)与decltype((x)):
    • decltype(x)是按照x的实际类型进行推导(会保留const),比如,若x为左值引用,则decltype(x)为左值引用,若x为右值引用,则decltype(x)为右值引用
    • decltype((x))会保留const,且左值/左右值引用均会推导为T&,但右值会推导为T,如decltype((1))会推导为int
  4. 尾置返回类型:
    C++98前,auto无法作为函数返回类型,C++11中,auto可通过尾置返回类型语法作为函数返回类型,C++14后,auto可以单独作为函数返回类型使用,
    尾置返回类型语法为:template<...> auto func_name() decltype(...) -> {},最终auto类型为decltype(...)的推导结果
    注:到目前C++23为止,C++函数仍必须确保使用auto作为返回类型时,所有参与返回类型推导的return语句必须推导出相同类型
  5. decltype的()内应为表达式(因此不能写成decltype(T)的形式),而func()这种自然也可以推导,但推导时无需调用func函数

测试代码链接:

C++11 auto 与 decltype

补充知识点(push_back系列与emplace系列的区别)

  1. 其实,STL库中push系列具备
  • void push_back (const value_type& val);

  • void push_back (value_type&& val);

    两种重载,而emplace系列则为:

  • template <class... Args> void emplace_back (Args&&... args);

    可变参数函数模板

实际上,通过上面的学习可以较清晰的推导出两者的区别:

  1. 在面对左值对象与右值对象时,两者都会调用对饮左值/右值引用版本,内部可能使用完美转发调用对应迭代器参数版本(取决于具体实现),因此,此情况无区别

  2. emplace版本实现了可变参数模板,所以可以直接传对象构造函数所需的参数,其与push系列的效率区别就在这里,emplace传参时,会逐层传参,直至构造对象时,调用一次构造函数;但push由于没有实现可变参数模板,只能先将参数构造为临时对象,再进行传参,这样,在构造节点时需要调用移动构造,因此,push系列会比emplace多调用一次移动构造

    注:上述提到的效率问题,其实不区分emplace传参时参数列表中参数是否为右值,因为使用可变参数模板天然就能做到比push少一次移动,但是再想深想一步的话,会发现,emplace传参时,对于右值参数,完美转发会保留其右值属性,最终构造函数可以匹配右值引用参数,从而使用移动语义,也就是说,emplace传参时,如果目标类型的构造函数针对左值和右值提供了不同的处理方式(如拷贝/移动),那么传递右值通常可以利用移动语义,减少拷贝开销

  3. 对于被 explicit 修饰的构造函数,不再支持通过该构造函数进行隐式类型转换。此时 push_back 无法直接传入可转换的参数,只能显式构造对象;而 emplace 系列可以直接传入构造参数,在容器内部直接调用 explicit 构造函数完成对象构造

作者复盘完美转发时的思考链条(作者自己的思考,仅参考,不建议当作标准记忆)

"万能引用 + 完美转发"旨在解决函数参数传递过程中类型丢失的问题(如下面示例,forwardExample传参右值,会被右值引用接收,

但由于右值引用是左值,所以process函数接收到的参数类型为左值引用,导致类型丢失)

进一步:为什么要保持右值类型呢?原因是,右值意味着"可以被移动",而左值意味着"必须被拷贝"

试想一个函数调用链,需要一直向下传递某个参数:

  1. 如果先开始的参数传递左值的话,到最后使用该参数时,函数内肯定不能move,会该左值变量失效,造成语义错误
  2. 如果先开始参数传递右值,且中途使用const T & ,到最后使用时并非只读,肯定不能操作,只能拷贝一份进行操作,但实际上,对于右值,没必要拷贝,直接移动就可以了(注:move(const T&)合法,但是结果为const T&&,后续拷贝时仍会调用拷贝而非移动,原因是const语义本身就包含了禁止被移动的含义)
  3. 思考到第二点后,自然会想到,那参数传递时直接使用T && ,不就可以保持右值语义了吗?缺点似乎就是无法接收左值类型,那就还需要写对应的左值重载
    • -》首先,会出现一个误区,一直T&&传参无法保持右值语义,因为右值引用类型为左值,所以T&&还需要在函数内使用move()进行传参来保持右值语义
    • -》只要做到move()进行传参来保持右值语义 + 用template void forwardExample(T&& arg) + 左值模板重载 不就可以了吗?
    • -》会发现,右值引用 + 模板其实就是万能引用,所以说,甚至可以不需要再写左值模板重载,不过不足就是会导致后续传参时无法区分参数的类型为左值还是右值,
    • -》由此,便出现了完美转发(forward模板)
    • -》推导出了万能引用 + 完美转发
  4. 引用折叠机制,3中推导出了万能引用的产生,但是,要如何实现 模板 + 右值引用 == 万能引用呢?

现了完美转发(forward模板)

  • -》推导出了万能引用 + 完美转发
  1. 引用折叠机制,3中推导出了万能引用的产生,但是,要如何实现 模板 + 右值引用 == 万能引用呢?

答案就是,需要引入引用折叠机制。对于T&&这种参与模板类型推导的形式,传入左值时,T会推导为V&,经过引用折叠后得到V&;传入右值时,T会推导为V,最终得到V&&。正是这种机制,使得"模板类型推导 + T&&"能够同时接收左值和右值,从而形成万能引用

相关推荐
小猪写代码1 小时前
C++中什么是类,什么是对象
c++
钓鱼的肝5 小时前
梳理(1-5)
c++·经验分享·笔记·算法·青少年编程
jimy16 小时前
readelf 查看“派生类”的vtable符号
开发语言·c++
蒸蒸yyyyzwd6 小时前
cpp 选手秋招学习笔记 day37 面经学习
c++·八股
YYYing.7 小时前
【C++进阶系列 (七)】C++ RTTI 深度剖析:从 typeid 到 dynamic_cast 的底层之旅
开发语言·c++·c/c++·rtti
钓鱼的肝8 小时前
csp-j-s总结(2)
c++·经验分享·笔记·算法·青少年编程
重生的黑客8 小时前
Qt 常用控件精讲(1):QWidget 核心属性全解 —— 从 geometry 到 qrc 与 QSS
c++·qt·qwidget·qss·常用控件
Hespethorn8 小时前
取 `X-Forwarded-For` 首段做频控 key,等于把限流开关交给了调用方
c++
米糕闯编程8 小时前
鱼香ros2(七)功能包组织C++节点
c++·ros2·cmakelists