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

2022-12-04 16:00:49 浏览数 (1)

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,而上面哪个,取得的则是一个指涉到临时对象的指针,而且这个对象会在循环迭代结束时被析构
}

0 人点赞