ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

[读书笔记]《Effective Modern C++》—— 类型推导、auto、decltype

2022-02-07 22:02:46  阅读:199  来源: 互联网

标签:const 推导 Effective 读书笔记 int auto param 类型 decltype


文章目录

前言

本文内容主要摘录自 《Effective Modern C++》,本文主要是将书中开头类型推导部分的内容放在一块进行说明,在再次品读这部分内容之前,对模板的认识就仅仅停留在模板是长这个样子的,使用的时候可以特化或者偏特化,对更深入的内容不曾有意识涉及。通过下面的内容对 C++ 的类型推导,以及 auto 和 decltype 等关键字也有了一定的了解,对于返回值后置的用法也不再感到摸不着头脑。也希望可以帮助到对相关类型推导及关键字同样有疑惑的同学,同样也更推荐直接去阅读原书的相关章节。

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

首先给出模板的一般定义,这里以函数模板为例:

template <typename T>
void f(ParamType param);

这里上面编译期会通过输入参数进行两个类型推导,一个是 T 的类型,另一个就是 ParamType。其中 T 的类型不仅依赖于输入参数的类型,还要依赖于 ParamType 的形式。对于 ParamType 的形式,一共分下面三种不同的情况来讨论。

情况1:ParamType 具有指针或者引用

template <typename T>
void f1(T& param);

template <typename T>
void f2(const T& param);

template <typename T>
void f3(T* param);

template <typename T>
void f4(const T* param);

int x = 27;
const int cx = x;
const int &rx = x;
const int* px = &x;

f1(x);  // T 类型:int, param 类型:int &
f1(cx); // T 类型:const int, param 类型:const int &
f1(rx); // T 类型:const int, param 类型:const int &

f2(x);  // T 类型:int, param 类型:const int &
f2(cx); // T 类型:int, param 类型:const int &
f2(rx); // T 类型:int, param 类型:const int &

f3(&x); // T 类型:int, param 类型:int*
f3(px); // T 类型:const int, param 类型: const int*

上面可以看到,如果 ParamType 的形式中就带有了 const,那么 T 的类型推导中就会忽略 const 属性。

情况2:ParamType 是万能引用

template <typename T>
void f(T&& param);

f(x);  // T 类型:int&, param 类型:int&
f(cx); // T 类型:const int&, param 类型:const int&
f(rx); // T 类型:const int&, param 类型:const int&

f(27); // T 类型:int, param 类型:int&&

万能引用会区别左值和右值,如果是左值,T 和 ParamType 都会被推导成左值引用,如果传入的是右值引用,则按照情况1的规则推导。

情况3:ParamType 非指针也非引用

template <typename T>
void f(T param);  // 按值传递

f(x);  // T 类型:int, param 类型:int
f(cx); // T 类型:int, param 类型:int
f(rx); // T 类型:int, param 类型:int

const int const* ptr = &x;
f(ptr); // T 类型:const int*, param 类型:const int*

因为是按值传递,无论传入什么 param 都将是一个新的副本,这会使 T 和 ParamType 忽略引用性及 cv 特性。因为原对象不能修改不代表新的副本是不可修改的,所以这里会失去 cv 特性。

数组实参

这里注意模板当是数组实参时,直接值传递,数组会退化成指针。但是引用传递,就会向其传递一个实际的数组类型。

template <typename T>
void f(T param);  // 按值传递,数组退化成指针

template <typename T>
void f(T& param);  // 按引用传递,数组类型,携带 size 信息

template <typename T, std::size_t N>
std::size_t arraySize(T(&)[N]) { // 这个编译期直接返回数组的元素个数
    return N;
}

函数实参

对应的,函数也会退化成指针。

void func(int, double)

template <typename T>
void f1(T param);  // 按值传递,函数退化成指针

template <typename T>
void f2(T& param);  // 按引用传递,函数推导成引用

f1(func); // void(*)(int ,double)
f2(func); // void(&)(int ,double)

条款二:理解 auto 的类型推导

以上 auto 的类型推导基本等同于模板推导,只是有一种情况是 auto 特殊的,就是初始化列表的情形 std::initializer_list<T>。
在 C++ 11 中为了支持统一的初始化,定义了初始化列表模板,并且只要是花括号的初始化,auto 推导类型就是 std::initializer_list,其也是一个模板,所以内部的数据类型要一致。 如果向对应的函数模板传入花括号,会直接编译报错

在类型推导中 auto 就相当于扮演了模板中的 T 这个角色。并且在函数返回值和 lambda 表达式的形参中使用 auto,仅仅是表示使用模板类型推导而非 auto 型别推导(所以直接传入初始化列表形式是编译不过的)。(仅仅表示,并不指导具体推导规则)。

使用 auto 的好处

  1. 首先对于一些冗长的类型可以少些代码
// 不适用 auto
std::function<bool(const std::unique_ptr<Widget>&,
                   const std::unique_ptr<Widget>&)>
func = [](const std::unique_ptr<Widget>& p1,
          const std::unique_ptr<Widget>& p2) {
              return *p1 < *p2;};

// 使用 auto 
auto func = [](const std::unique_ptr<Widget>& p1,
          const std::unique_ptr<Widget>& p2) {
              return *p1 < *p2;};
  1. 避免出现显式类型不当导致的代码问题
// 示例1
vector<int> v;
// 不使用 auto
unsigned sz = v.size(); 
// 使用 auto
auto sz = v.size();

// 示例2
map<string, int> m;
// 不使用 auto
for(const pair<string, int>& p: m) {...}
// 使用 auto
for(const auto& p: m) {...}

上面示例1显式定义 sz 变量为 unsigned 类型,在 32 位和 64 位机器上都是 32 位的,但是 v.size() 实际的返回类型为 size_t,其在 32 和 64 位机器上分别是 32 位和 64 位,这可能就会引起问题。

示例2 则是因为 map 的键是不可修改的,所以实际的类型为 pair<const string, int>, 如果显式声明为 pair<string, int> 类型,编译器会把所有的对象都拷贝一遍,然后把 p 这个引用绑定到临时对象上,每次迭代结束,临时对象再析构一次,效率上大打折扣。

显式类型初始化

一个比较特别的例子是 vector 类型用 operator[] 访问,返回的不是一个 bool 类型,因为底层优化了 bool 的类型,使用 1 bit存储,返回的是 vector::reference, 其是一个代理类,可以进行隐士转换成 bool 类型。需要进行一个显式的类型定义,使 auto 推导成我们想要的类型。

vector<bool> b;
auto data = b[1]; // auto 推导成 vector<bool>::reference 类型
auto data = static_cast<bool>(b[1]); // 强制推导成 bool 类型

包括一些人为要的类型隐式转换,带显式类型的初始化用法强制 auto 推导成我们想要的类型。

条款三:理解 decltype

decltype 可以给定一个变量或者是表达式,会返回对应表达式或者变量的确切类型。

bool f(const Widget& w);
decltype(w); // const Widget&
decltype(f); // bool(const Weight&)

vector<int> v{1,2,3};
decltype(v);  // vector<int>
decltype(v[0]);  // int&

返回值类型后置

// C++11
template<typename C, typename I>
auto f1(C& c, I i) {
    return c[i];
}

template<typename C, typename I>
auto f2(C& c, I i) -> decltype(c[i]) {
    return c[i];
}

// C++14
template<typename C, typename I>
decltype(auto) f3(C& c, I i) {
    return c[i];
}

这里 auto 指定为返回的函数进行模板类型推导,并且其相当于 T,这里是按值传递,f1 函数如果不使用 decltype,会损失 cv 与引用特性,不用 decltype 类型推导,auto 返回的就是 int 类型,是一个右值,在使用时如果对其结果赋值就会编译不通过。

f2 则没有这个问题,decltype 会推导出其类型为 int&, 左值引用。并且这样返回值类型后置的好处是可以使用函数形参了。

上面的返回值是一个需要注意的点,还有一个注意的点是我们传入的模板类型,这里是左值引用,并且是非常量左值引用,就没办法传入一个右值,重载一个右值引用实参时一个办法,另一个更好的办法就是万能引用,为了能传递右值类型,需要配合 std::forward 完美转发一起使用。

template<typename C, typename I>
auto f4(C&& c, I i) -> decltype(std::forward<C>(c)[i]) {
    return std::forward<C>(c)[i];
}

总结

上面通过介绍 C++ 的模板推导规则,介绍了在非引用和指针的情况下(值传递,所以会省略 cv 特性),普通指针或引用(保留 cv 及数组特性),万能引用(主要区分左右值) 3 种情况下的相关推导规则。
引出了 auto 基本与其上一致,唯一区别是为了统一初始化,增加了对初始化列表类型的推导和支持。
最后 decltype 就是如实地返回变量或者表达式的类型,更多地是用在返回值类型后置(需要使用形参)的情况。

标签:const,推导,Effective,读书笔记,int,auto,param,类型,decltype
来源: https://blog.csdn.net/Chris_zhangrx/article/details/122815070

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有