[读书笔记]《Effective Modern C++》—— 类型推导、auto、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 就是如实地返回变量或者表达式的类型,更多地是用在返回值类型后置(需要使用形参)的情况。

上一篇:人人都是 Serverless 架构师 | 弹幕应用开发实战


下一篇:常见Post请求与实现