前言

之前分享过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的优先级处理Wdiget,函数期望highPriority类型是bool
processWidget(w, highPriority);//未定义行为!!!

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

要点1:“隐形”的代理类型可以导致auto根据初始化表达式推导出“错误的”类型。

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

auto highPriority = static_cast<bool>(feature(w)[1]);
要点2:auto推导出“错误的”类型时可以进行强制类型转换,让auto强制推导出你想要的类型。

吴尼玛
32 声望12 粉丝

记问之学