乐趣区

关于c++:学懂现代CEffective-Modern-C之类型推导和auto

前言

之前分享过 Scott Meyers 的两本书《Effective C++》和《More Effective C++》。这两本书对我深刻学习 C ++ 有着很大的帮忙,倡议所有想进阶 C ++ 技术的同学都能够看看。然而,这两本书是大神 Scott 在 C ++11 之前出的,而 C ++11 对于 C ++ 社区来说是一次重大的改革,被称为古代 C ++,用以辨别 C ++11 之前的传统 C ++。

好在 Scott 在之后也带来了全新的《Effective Modern C++》,其中也是连续了作者一贯的格调和品质。带来了 42 个独家技巧,助你改善 C ++11 和 C ++14 的高效用法(封面语)。

本文首先就带同学们一起看看这本书的前两章——类型推导和 auto。

首先申明本文只是做知识点的总结,书中有更具体的推导和解说过程,感兴趣的同学还是强烈建议大家去读原书。

类型推导

条款 1:了解模板类型推导

模板类型推导是 C ++11 利用最宽泛的个性之一——auto 的根底。所以,了解 auto 的推导规定和正确应用形式的根底就是了解模板类型推导的规定。

先来看看模板和其调用的个别模式。

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

f(expr);

这里须要 T 的类型推导后果,依赖于 expr 的类型和 ParamType 的模式。其中,ParamType 的模式须要分三种状况探讨。

  • 状况 1:ParamType 是个指针或者援用,但不是万能援用(模式如 T &&)。

在这种状况下,模板类型推导具备以下规定:

  1. expr 的援用属性会被疏忽。
  2. 疏忽 expr 的援用性后,expr 的类型和 ParamType 的类型进行模式匹配,由此决定 T 的类型。

举个例子:

// 申明模板
template<typename T>
void f(T& param);

// 申明变量
int a = 1;
const int ca = a;
const int& cra = a;

// 调用模板
f(a);   // a 的类型是 int,T 的类型是 int,param 的类型是 int&。f(ca);  //ca 的类型是 const int,T 的类型是 const int,param 的类型是 const int&。f(cra); //cra 的类型是 const int&,T 的类型是 const int,param 的类型是 const int&。

要点 1:在模板类型推导过程中,具备援用类型的实参会被当成非援用类型来解决。

  • 状况 2:ParamType 是个万能援用。

在这种状况下,模板类型推导规定如下:

  1. 如果 expr 是个左值,则 T 和 ParamType 都会被推到为左值援用。
  2. 如果 expr 是个右值,则和状况 1 中的推导规定雷同。

举个同状况 1 相似的例子:

// 申明模板
template<typename T>
void f(T&& param);

// 申明变量
int a = 1;
const int ca = a;
const int& cra = a;

// 调用模板
f(a);   // a 是左值,类型是 int,T 的类型是 int&,param 的类型是 int&。f(ca);  //ca 是左值,类型是 const int,T 的类型是 const int&,param 的类型是 const int&。f(cra); //cra 是左值,类型是 const int&,T 的类型是 const int&,param 的类型是 const int&。f(1);   // 1 是右值,类型是 int,T 的类型是 int,param 的类型是 int&&。

要点 2:对万能援用形参进行推导时,左值实参会进行非凡解决。

  • 状况 3:ParamType 即不是指针也不是援用。

这种状况就是按值传递,其指标推导规定如下:

  1. expr 的援用属性会被疏忽。
  2. 疏忽 expr 的援用性后,如果 expr 还具备 const 或 volatile 属性,也会被疏忽。

还是看一下例子:

// 申明模板
template<typename T>
void f(T param);

// 申明变量
int a = 1;
const int ca = a;
const int& cra = a;

// 调用模板
f(a);   // a 的类型是 int,T 和 param 的类型都是 int。f(ca);  //ca 的类型是 const int,T 和 param 的类型都是 int。f(cra); //cra 的类型是 const int&,T 和 param 的类型都是 int。

要点 3:对按值传递的形参进行推导时,实参中的 const 或 volatile 属性,也会被疏忽。

  • 有一个非凡状况须要留神的就是数组或函数做模板的实参的状况。

要点 4:数组或函数类型的实参在模板推导过程中会进化为对应的指针。除非形参 param 是按援用传递的,这时就会被推导为数组或函数的援用。

条款 2:了解 auto 类型推导

如果你曾经熟练掌握了后面模板类型推导的规定,那么祝贺你也根本把握了 auto 类型的推导了。因为除了一个非凡状况外,auto 类型推导和模板类型推导的规定是一样的。

先看和模板类型推导一样规定的示例:

auto a = 1;         // a 的类型是 int
const auto ca = a;  //ca 的类型是 const int
const auto& cra = a;//cra 的类型是 const int&
auto&& ra1 = a;     //ra1 的类型是 int&
auto&& ra2 = ca;    //ra2 的类型是 const int&
auto&& ra3 = 1;     //ra3 的类型是 int&&

惟一的非凡状况就是在应用了 C ++11 引入的对立初始化——大括号初始化表达式时。如果向模板传入一个大括号初始化表达式,则无奈编译通过。而 auto 会将其推导为 std::initializer\_list。

举例如下:

// 申明模板
template<typename T>
void f(T param);

f({1, 2, 3}); // 无奈编译通过

auto a = {1, 2, 3}; // a 的类型是 std::initializer_list<int>

另外,还有一个要留神的点是:在 C ++14 中能够在函数的返回值或 lambda 表达式的形参中应用 auto,但这里的 auto 应用的是模板类型推导,而不是 auto 类型推导。所以如果在这种状况下应用大括号初始化表达式也是无奈编译通过的。

条款 3:了解 decltype

要点 1:在绝大多数状况下,decltype 会返回变量或表达式确切的类型。

在 C ++11 中,decltype 的主要用途就在于申明那些返回值类型依赖于形参类型的函数模板。举个例子,咱们写一个模板函数 f,其形参是一个反对方括号下标语法(即“[]”)的容器和一个下标,并在返回下标操作后果前进行合法性验证。函数的返回值类型须要与下标操作后果的返回值类型雷同。其最终实现如下:

//C++11 版
template<typename Container, typename Index>
auto f(Container&& c, Index i) -> decltype(std::forward<Container>(c)[i]) {checkInvalid();// 合法性验证
    return std::forward<Container>(c)[i];
}

//C++14 版
template<typename Container, typename Index>
decltype(auto) f(Container&& c, Index i) {checkInvalid();// 合法性验证
    return std::forward<Container>(c)[i];
}

// 应用
auto str = f(makeStringDeque(), 5);// 其中 makeStringDeque 是一个返回 std::deque<std::string> 的工厂函数 

条款 4:把握查看类型推导后果的办法

要点 1:利用 IDE 编辑器、编译器错误信息和 Boost.TypeIndex 库经常可能查看到推导失去的类型。

要点 2:有些工具得出的后果可能无用或者不精确。所以,了解 C ++ 类型推导规定是必要的。

auto

条款 5:优先选用 auto,而非显式类型申明

C++11 新增的 auto,最大的作用就是让咱们远离了那些啰嗦的显示类型申明。比方我用 std::function 定义一个如下的函数:

std::function<bool(const std::unique_ptr<Widget>&, 
                    const std::unique_ptr<Widget>&)> 
    derefUPLess = [](const std::unique_ptr<Widget>& p1, 
                    const std::unique_ptr<Widget>& p2)
                    {return *p1 < * p2;};

能够看到这个定义写起来真是啰嗦,且还容易一不小心写错。如果咱们用 auto 来定义则能够写成:

auto derefUPLess = [](const std::unique_ptr<Widget>& p1, 
                    const std::unique_ptr<Widget>& p2)
                    {return *p1 < * p2;};

更有甚者,在 C ++14 中 auto 能够应用在 lambda 表达式的形参中,于是咱们能够失去一个能够利用于任何相似指针之物指涉到的值的比拟函数,如下:

auto derefLess = [](const auto& p1, 
                    const auto& p2)
                    {return *p1 < * p2;};

而且,通常状况下,std::function 对象个别都会比应用 auto 生命的变量应用更多的内存。所以,在能应用 auto 的状况下,咱们通常都应该抉择应用 auto。

要点 1:auto 变量必须初始化,基本上对会导致兼容性和效率问题的类型不匹配景象免疫,还能够简化重构流程,通常也比显式指定类型要少打一些字。

条款 6:当 auto 推导的类型不符合要求时,应用带显式类型的初始化物习惯用法

在条款 2 中,咱们曾经见识了 auto 推导的一个非凡状况。同样,还存在着一个广泛的法则是,“隐形”代理类和 auto 无奈和平共处。

咱们须要先认识一下“隐形”代理类。所谓代理类,就是指为了模仿或加强其余类型的类。代理类在 C ++ 中很常见,比方规范库中的智能指针就是将资源管理嫁接到裸指针上。举个例子,通常咱们会认为 std::vector<T> 的 operator[] 会返回 T &。但实际上,std::vector<bool> 的 operator[] 的返回值类型是 std::vector<bool>::reference,它就是一个“隐形”的代理类。请看以下代码:

std::vector<bool> features(const Widget& w);// 返回的 vector 种每一个 bool 值都代表着 Widget 是否提供一个特定的性能

Widget w;

auto highPriority = feature(w)[1];// 第一个 feature 示意这个 Widget 是否具备高优先级

processWidget(w, highPriority);// 未定义行为!!!

为什么最初一句是一个未定义行为呢?因为这里 auto 推导的类型是 std::vector<bool>::reference。而这个代理类可能会导致 highPriority 含有一个空悬的指针(具体起因设计标准库的实现,细节请看书)。

要点 1:“隐形”的代理类型能够导致 auto 依据初始化表达式推导出“谬误的”类型。

解决这个问题的办法就是强制进行一次类型转换。咱们将下面出问题的语句替换为以下语句,就能够解决这个未定义行为:

auto highPriority = static_cast<bool>(feature(w)[1]);

要点 2:auto 推导出“谬误的”类型时能够进行强制类型转换,让 auto 强制推导出你想要的类型。

退出移动版