前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >你理解模板型别推导【C++】的原理吗?

你理解模板型别推导【C++】的原理吗?

作者头像
用户9831583
发布2022-12-04 16:00:49
5200
发布2022-12-04 16:00:49
举报
文章被收录于专栏:码出名企路码出名企路

Part1第1章 型别推导

1条款1:理解模板型别推导

//一般的函数模板声明

代码语言:javascript
复制
//一般的函数模板声明
template<typename T>
void fun(ParamType param);
fun(expr);//从expr中推导T和paramType的型别

//情况1:param是指针或引用, 但不是万能引用

代码语言:javascript
复制
//情况1:param是指针或引用, 但不是万能引用
template<typename T>
void f(T& param)
{
    cout<<"fT1: "<<param<<endl;
}

template<typename T>
void fPtr(T* param)
{
    cout<<"fT2: "<<*param<<endl;
}

//情况2:param是万能引用

代码语言:javascript
复制
//情况2:param是万能引用
template<typename T>
void fW(T&& param)//万能引用
{
    //可以接收左值和右值
    param = 40;
    cout<<"fW: "<<param<<endl;
}

//情况3:param非指针也非引用

代码语言:javascript
复制
//情况3:param非指针也非引用
template<typename T>
void fF(T param)
{
    //按值传递, param是实参的一个副本一个全新的对象
   // param = "50";
    cout<<"fF: "<<param<<endl;
}

//情况4:利用声明数组引用能力创造一个模板,推导数组含有的元素个数

代码语言:javascript
复制
//情况4: 利用声明数组引用能力创造一个模板,推导数组含有的元素个数
template<typename T,std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{   
    //在编译期常量形式返回数组尺寸
    return N;
}

//情况5:函数实参

代码语言:javascript
复制
//情况5: 函数实参
template<typename T>
void fFFunc(T param)
{
    //按值传递, param是实参的一个副本一个全新的对象
   // param = "50";
    cout<<"fFFunc: "<<param<<endl;
}
template<typename T>
void fYYunc(T& param)
{
    cout<<"fYYunc: "<<param<<endl;
}
void someFunc(int a,double b)
{
    cout<<"SF: "<<a<<" "<<b<<endl;
}

客户端测试:

代码语言:javascript
复制
int main()
{
    int x = 27;//x的型别是int
    const int cx = x;//cx的型别是 const int
    const int& rx = x;//rx是x的型别为 const int 的引用
    
    //情况1:

    //对模板函数调用型别的推导
    f(x); //T的型别是int , param的型别是 int&
    f(cx);//T的型别是const int, param的型别是const int&
    f(rx);//T的型别是const int, param的型别是const int&

    const int *px = &x;//px是指涉到x的指针,型别为 const int
    fPtr(&x);//T的型别是int param的型别是int*
    fPtr(px);//T的型别是const int, param的型别是const int*

    //情况2:

    fW(x);//x是左值 T的型别是int&, param的型别也是int&
    //只读const 编译不过
   // fW(cx);//cx是左值 T的型别是int&, param的型别也是int&
   // fW(rx);//cx是左值 T的型别是int&, param的型别也是int&
    fW(27);//27是个右值 T的型别是int param的型别变成了 int&&
    int &&r = 100;//r绑定到一个右值,但是r变量本身是个左值
    fW(r);

    //情况3: 区别与情况2中注释部分
    //请注意:及时cx rx是const的,param仍然不具有const型别,因为param是
    //完全独立于cx和rx存在的对象,它们的一个副本,是可以修改的。

    fF(x);//T和param都是int
    fF(cx);//T和param都是int
    fF(rx);//T和param都是int
    
    const char* const ptr = "FUN FUN";//ptr是指涉到const对象的const指针
    fF(ptr);//传递型别为const char *const的实参
    
    //数组实参
    const char name[] = "JPJPJP";//name的类型是const char[13]
    const char *ptrToName = name ;//数组退化为指针
    fF(name);//name是哥数组,但是T的型别却被推导成 const char*
    f(name);//按引用传递

    //情况4:

    int keyVals[] = {1,2,3,4,5,6};
    int mappedVals[arraySize(keyVals)];
    cout<<arraySize(keyVals)<<endl;
    //现代C++
    std::array<int,arraySize(keyVals)> mapped;

    //情况5:
    fFFunc(someFunc);//函数指针 void (*)(int,double)
    fYYunc(someFunc);//函数引用 void (&)(int,double)

}

输出:

fT1: 27 fT1: 27 fT1: 27 fT2: 27 fT2: 27 fW: 40 fW: 40 fW: 40 fF: 40 fF: 27 fF: 40 fF: FUN FUN fF: JPJPJP fT1: JPJPJP 6 fFFunc: 1 fYYunc: 1

2条款2:理解auto型别推导

auto类别推导其实就是模板类别推导,只不过模板类别推导涉及模板、函数和形参,而auto和它们无关

主要思想:

//条款1:函数模板推导

// template

// void f(ParamType param);

// f(expr);

//条款2 auto应用在 条款1中可以如下解释:

//1, auto 扮演了模板中的 T 这个角色

//2, 变量的型别修饰词扮演的是 ParamType 的角色

//情况1:

代码语言:javascript
复制
//情况1: 
auto x =27;//x的型别修饰词是其自身
const auto cx =x;//x的型别修饰词为 const auto
const auto& rx =x;//型别修饰词变成了 const autp&

//对应的模板类别
template<typename T>
void func_for_x(T param)
{
    cout<<"T: "<<param<<endl;
}

template<typename T>
void func_for_cx(const T param)
{
    cout<<"const T: "<<param<<endl;
}

template<typename T>
void func_for_rx(const T& param)
{
    cout<<"const T&: "<<param<<endl;
}

// 情况2:右值引用

代码语言:javascript
复制
// 情况2: 右值引用
auto&& uref1 = x;//x的型别是int 左值 所以 uref1的型别是 int&
auto&& uref2 = cx;//cx的型别是 const int, 左值 所以 uref2的型别是 const int&
auto&& uref3 = 27;//27的型别是 int 右值 所以 uref3的型别是 int&&

//情况3:数组退化成指针

代码语言:javascript
复制
//情况3:数组退化成指针
const char name[] = "IIIIII";//name的型别是 const char[7]
auto arr1 = name;//arr1的型别 const char*
auto& arr2 = name;//arr2的型别 const char (&)[7]
void someFunc(int a,double b)//someFunc是个函数 型别是 void(int,double)
{
    cout<<"someFunc: "<<a<<" "<<b<<endl;
}
auto func1 = someFunc;//func1的型别是 void (*)(int,double)
auto& func2 = someFunc;//func1的型别是 void (&)(int,double)

//情况4:声明int的方式

代码语言:javascript
复制
//情况4: 声明int的方式
//C++ 98 
int x1 = 27;
int x2(27);
//C++11
int x3 = {27};
int x4{27};
//转换成 auto 
auto x11 = 27;
auto x22(27);//类型int 值 27
auto x33 = {27};//型别是 std::initializer_list<T> 值是 {27} 里面的类型必须一致
auto x44{27};//同上

//情况5:auto 返回值和 lambda表达式

代码语言:javascript
复制
//情况5:auto 返回值和 lambda表达式
//必须使用模板型别推导而不是 auto 型别推导
// auto createInitList()
// {
//     return {1,2,3};//错误, 
// }

// std::vector<int> v;
// auto resetV = [&v](const auto& newValue) {
//     v = newValue;
// };
// resetV({1,2,3});//错误

测试用例:

代码语言:javascript
复制
int main()
{
    //测试1:
    func_for_x(27);//param的型别就是 x 的型别
    func_for_cx(x);//param的型别就是 cx 的型别
    func_for_rx(x); //param的型别就是 rx 的型别

    //测试2:
    func_for_x(name);//param的型别就是 x 的型别
    func_for_cx(arr1);//param的型别就是 cx 的型别
    func_for_rx(arr2); //param的型别就是 rx 的型别

    func1(1,2);
    func2(3,4);

    //测试4
    func_for_x(x11);
    func_for_x(x22);
    func_for_x(x3);
    func_for_x(x4);
}

//要点速记

//1, 一般下, auto型别推导和模板型别推导一样,但是 auto型别推导会假定用大括号括起来的初始化表达式代表一个

//std::initializer_list, 但是模板型别推导却不会

//2, 在函数返回值或 lambda的形参中使用 auto,意思是使用模板型别推导而不是 auto 型别推导

3条款3:理解 decltype

//decltype作用:对于给定的名字或表达式, decltype 能告诉你该名字或表达式的型别

//情况1:鹦鹉学舌,返回给定的名字或表达式的确切型别而已

代码语言:javascript
复制
//情况1: 鹦鹉学舌,返回给定的名字或表达式的确切型别而已
class Widget{

};
const int i = 0; //decltype(i) 是 const int
bool f(const Widget& w);//decltype(w)是 const Widget&, decltype(f)是bool(const Widget&)
struct Point{ //decltype(Point::x)是 int
    int x,y;
};
Widget w;//decltype(w)是 Widget
if(f(w))//decltype(f(w))是Widget

//std::vector的简化版
template<typename T>
class vector{
    public:
        T& operator[](std::size_t index);
};
vector<int> v;//decltype(v)是vector<int>
if(v[0] == 0) //decltype(v[0]) 是 int&

//情况2:使用 decltype 计算返回值型别

//一般来说,含有型别 T 的对象的容器,其 operator[] 会返回 T&, 注意一点 std::vector对应的 operator[] 并不返回 bool& ,而是返回一个全新对象。

代码语言:javascript
复制
//情况2:使用 decltype 计算返回值型别
//一般来说,含有型别 T 的对象的容器,其 operator[] 会返回 T&, 注意一点 std::vector<bool> 对应的 operator[] 并不返回 bool&
//而是返回一个全新对象
void authenticateUser()
{
    cout<<"I am lyy"<<endl;
}
template<typename Container, typename Index>
auto authAndAccess11(Container& c, Index i) -> decltype(c[i])//C++11
{
    authenticateUser();
    return c[i];
}
//以上好处是:使用这样一个声明之后,operator[]返回值是什么型别,authAndAccess的返回值就是什么型别
template<typename Container, typename Index>
auto authAndAccess14(Container& c, Index i)//C++14 
{
    authenticateUser();
    return c[i];  //返回值型别是根据 c[i] 推导出来
}

//以上设计会有隐患:大多数含有 型别 T 的对象的容器的 operator[] 会返回 T&, 初始化表达的引用性会被忽略
std::deque<int> d;
//验证用户, 并返回d[5], 然后将其赋值为10
authAndAccess11(d,5) = 10; // 没有问题,可以通过编译
authAndAccess14(d,5) = 10; //无法通过编译
//d[5] 返回的是 int& , 但是对 authAndAccess 的返回值实施 auto 型别推导将剥去引用,这么一来返回值型别就成了 int 作为函数的返回值,该 int 是个右值, 所以上述代码其实是尝试将 10 赋给一个右值 int, C++中无法通过编译

//如上改进:authAndAccess,指定 这个函数的返回值型别与表达式 c[i]返回的型别完全一致
//如下:auto指定了欲实施推导的型别,推导过程中采用的是 decltype 的规则
template<typename Container, typename Index>
decltype(auto)
authAndAccess1414(Container& c, Index i)
{
    authenticateUser();
    return c[i];
}

//情况3:变量声明的场合,在初始化表达式处应用 decltype 型别推导规则

代码语言:javascript
复制
// //情况3:变量声明的场合,在初始化表达式处应用 decltype 型别推导规则
Widget w;
const Widget& cw = w;
auto myWidget1 = cw;//auto型别推导:myWidget1的型别是 Widget
decltype(auto) myWidget2 = cw;//decltype型别推导:myWidget2的型别是 const Widget&

//情况2的改进:容器的传递方式是非常量的左值引用,因为返回该容器的某个元素的引用,就意味着允许客户对容器进行修改,这也意味着无法向容器中传递右值容器,右值是不能绑定到左值引用的。

代码语言:javascript
复制
//万能引用的实现
template<typename Container, typename Index>
decltype(auto)
authAndAccessWN14(Container&& c, Index i)//C++14
{
    authenticateUser();
    return std::forward<Container>(c)[i];
}

//万能引用的实现
template<typename Container, typename Index>
auto
authAndAccessWN11(Container&& c, Index i)
-> decltype(std::forward<Container>(c)[i])//C++11
{
    authenticateUser();
    return std::forward<Container>(c)[i];
}

// //情况4:函数返回值

代码语言:javascript
复制
// //情况4: 函数返回值
decltype(auto) f1()
{
    int x =0;
    return x;//decltype(x)是int,所以f1 返回的是int
}

decltype(auto) f2()
{
    int x =0;
    return (x);//decltype(x)是int&,所以f2 返回的是int&
}
//但是,注意 f2返回的是一个局部变量的引用,未定义报错

测试用例:

代码语言:javascript
复制
int main()
{
    //测试1:
    std::vector<int> c = {1,2,3};//条款2的讲解
    cout<<authAndAccess11(c,1)<<endl;
    cout<<authAndAccess14(c,1)<<endl;

    std::deque<int> d;
    authAndAccess11(d,5) = 10;
    //authAndAccess14(d,5) = 10;
    authAndAccess1414(d,5) = 100;
    cout<<d[5]<<endl;

    //测试2:
    //左值
    cout<<authAndAccessWN11(c,2)<<endl;
    cout<<authAndAccessWN14(c,2)<<endl;
    //右值
    auto c_new = std::move(c);
    cout<<authAndAccessWN11(c_new,0)<<endl;
    cout<<authAndAccessWN14(c_new,0)<<endl;

    //测试3:
    cout<<f1()<<endl;
    cout<<f2()<<endl;
}

I am lyy 2 I am lyy 2 I am lyy I am lyy 100 I am lyy 3 I am lyy 3 I am lyy 1 I am lyy 1 0 Segmentation fault

//要点速记

//1, 绝大多数情况下,decltype 会得出变量或表达式的型别而不作任何修改

//2, 对于型别为 T 的左值表达式,除非该表达式仅有一个名字,decltype总是得出型别 T&

//3, C++14 支持 decltype(auto) ,和auto一样,它会从其初始化表达式出发来推导型别,但是它的型别推导使用的是 decltype的规则

4条款4:掌握查看型别推导结果的方法

//查看型别推导的三个阶段:撰写代码阶段,编译阶段和运行阶段

//撰写代码阶段

代码语言:javascript
复制
//撰写代码阶段
const int theAnswer = 42;
auto x = theAnswer;//x的型别推导是 int
auto y = &theAnswer;//y的型别推导是 const int*

//编译器诊断信息

代码语言:javascript
复制
//编译器诊断信息
//如要查看上面 x和y推导而得到的型别, 先声明一个类模板,但不去定义
template<typename T>
class TD;
//结果可想而知:只要试图实现该模板,就会诱发一个错误信息,原因是找不到实现模板所需要的定义

//运行时输出

代码语言:javascript
复制
//运行时输出
template<typename T>
void f(const T& param)
{
    using std::cout;
    cout<<"T =  "<<typeid(T).name()<<endl;
    cout<<"param = "<<typeid(T).name()<<endl;
}
class Widget
{

};

//测试

代码语言:javascript
复制
//测试
int main()
{   
    //测试1
    // TD<decltype(x)> xType;
    // TD<decltype(y)> yType;
    
    //测试2: 运行时输出型别
    std::cout<<typeid(x).name()<<endl;
    std::cout<<typeid(y).name()<<endl;

    //测试3:
    Widget *w = new Widget;
    std::vector<Widget*> createVec = {w,w,w};
    const auto vw = createVec;
    if(!vw.empty())
    {
        f(&vw[0]);
    }
}

结果:

i PKi T = PKP6Widget param = PKP6Widget

Part2第2章 auto

//如何引导 auto 得出正确的结果

5条款6:优先选用auto,而非显式型别声明

//情况1:没有初始化值

代码语言:javascript
复制
//情况1:没有初始化值
int x; //它的值是不确定的
//使用迭代器的提领结果来初始化局部变量:
template<typename It>
void dwim(It b, It e)
{
    while( b != e)
    {
        typename std::iterator_traits<It>::value_type //currValue的型别是这么一大串
             currValue = *b;
    }
}
//改进:用auto ,其型别都推导自其初始化物,所以它们必须初始化
auto x3 = 0;
template<typename It>
void dwimAuto(It b, It e)
{
    while( b != e)
    {
        auto currValue = *b;
    }
}

//情况2:auto使用了型别推导,就可以用它来表示只有编译器才掌握的型别

代码语言:javascript
复制
//情况2:auto使用了型别推导,就可以用它来表示只有编译器才掌握的型别
class Widget
{

};
auto derefUPLess =
 [](const std::unique_ptr<Widget>& p1,
    const std::unique_ptr<Widget>& p2)
{
    return *p1 < *p2;
}
//使用C++4 可以在 lambda 表达式的形参中使用auto了
auto derefLess = 
[](const auto& p1,
    const auto& p2)
{
    return *p1 < *p2;
}

//情况3:auto 大智慧

代码语言:javascript
复制
//情况3:auto 大智慧
//以下代码存在什么隐患呢?
std::unordered_map<std::string, int> m;
for(const std::pair<std::string,int>& p : m)
{
    
}
//std::unordered_map的键值部分是 const,所以哈希表中的 std::pair的型别并不是
//std::pair<std::string,int>,而应是 std::pair<const std::string, int>,可是
//上面的循环中,用以声明变量 p 的型别并不是这个。因此编译器需要将const 转换成 非const

//转换原理:对 m中的每个对象都做一次复制操作,形成一个 p想要绑定的型别的临时对象,
//然后把 p 这个引用绑定到该临时对象,在循环的每次迭代结束时,该临时对象都会被析构一次。

//改进:使用 auto化解
for(const auto& p : m)
{
    //1,如果对 p取地址,肯定会取得一个指涉 到 m中的某个元素的指针
    //2,而上面哪个,取得的则是一个指涉到临时对象的指针,而且这个对象会在循环迭代结束时被析构
}
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-07-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码出名企路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Part1第1章 型别推导
    • 1条款1:理解模板型别推导
      • 2条款2:理解auto型别推导
        • 3条款3:理解 decltype
          • 4条款4:掌握查看型别推导结果的方法
          • Part2第2章 auto
            • 5条款6:优先选用auto,而非显式型别声明
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档