类型推导
条款1: 理解模板推导
T 被推导成 int ,ParamType 被推导成 const int& 。
而当我们有代码
T 的类型不仅和 expr 的类型独立, 而且还和 ParamType 的形式独立。 下面是三个例子:
- ParamType 是一个指针或者是一个引用类型, 但并不是一个通用的引用类型 。
- ParamType 是一个通用的引用 。
- ParamType 既不是指针也不是引用 。
1 | template<typename T> |
ParamType 是个非通用的引用或者是一个指针。
当ParamType是一个引用类型或是一个指针,但是并非是一个通用的引用类型,则类型推导过程如下 :
1、如果expr的类型是个引用,则忽略引用部分。
2、然后利用expr的类型和ParamType对比判断T的类型。
例如,我们有
1 | template<typename T> |
则param和T在不同的调用下面的类型推导如下:
1 | f(x); // T是int, param的类型时int& |
这里的引用进行忽略。
如果我们把 f 的参数类型从 T& 变成 const T& , 情况就会发生变化,由于 param 的声明是 const 引用的, cx 和 rx 的 const 特性会被保留, 这样的话 T 的 const 特性就没有必要了。
1 | template<typename T> |
如果param是一个指针(或者指向const的指针)而不是引用,情况也类似
1 | template<typename T> |
ParamType 是个通用的引用( Universal Reference)
- 如果 expr 是一个左值, T 和 ParamType 都会被推导成左值引用。 这有些不同寻常。 第一, 这是模板类型 T 被推导成一个引用的唯一情况。 第二, 尽管 ParamType 利用右值引用的语法来进行推导, 但是他最终推导出来的类型是左值引用。
- 如果 expr 是一个右值, 那么就执行“普通”的法则( 第一种情况)
1 | template<typename T> |
ParamType 既不是指针也不是引用
当 ParamType 既不是指针也不是引用, 我们把它处理成pass-by-value:
1 | template<typename T> |
这就意味着 param 就是完全传给他的参数的一份拷贝——一个完全新的对象。 基于这个事实可以从 expr 给出推导的法则:
1 | int x = 27; // 和之前一样 |
注意尽管 cx 和 rx 都是 const 类型, param 却不是 const 的。 这是有道理的。 param 是一个和 cx 和 rx 独立的对象——一个 cx 和 rx 的拷贝。
数组参数
就是数组类型和指针类型是不一样的, 尽管它们通常看起来是可以替换的。 一个最基本的幻觉就是在很多的情况下, 一个数组会被退化成一个指向其第一个元素的指针。 这个退化的代码常常如此:
1 | const char name[] = "J. P. Briggs"; // name的类型是const char[13] |
在这里, const char 指针 ptrToName 使用 name 初始化, 实际的 name 的类型是 constchar[13] 。 这些类型( const char 和 const char[13] ) 是不一样的, 但是因为数组到指针的退化规则, 代码会被正常编译。
如果一个数组传递给一个安置传递的模板参数会如何?1
2
3
4template<typename T>
void f(T param); // 模板拥有一个按值传递的参数
f(name); // T和param的类型会被推到成什么呢?
因为数组参数声明会被当做指针参数, 传递给模板函数的按值传递的数组参数会被退化成指针类型。 这就意味着在模板 f 的调用中, 模板参数 T 被推导成 const char* :
1 | f(name); // name是个数组, 但是T被推导成const char* |
若我们使用引用则
1 | template<typename T> |
然后传递一个数组给他则
1 | f(name); // 传递数组给f |
T最后推导出来的实际类型是数组。类型推导包括了数组的长度, 所以在这个例子里面, T 被推导成了 const char [13] , 函数 f 的参数( 数组的引用) 被推导成了 const char(&)[13]
声明数组的引用可以使的创造出一个推导出一个数组包含的元素长度的模板:
1 | // 在编译的时候返回数组的长度( 数组参数没有名字, |
函数模板
数组并不是C++唯一可以退化成指针的东西。 函数类型可以被退化成函数指针 。
1 | void someFunc(int, double); // someFunc是一个函数 |
总结 |
---|
在模板类型推导的时候,有引用特性的参数的引用特性会被忽略 |
在推导通用引用参数的时候, 左值会被特殊处理 |
在推导按值传递的参数时候, const 和/或 volatile 参数会被视为非 const 和 非 volatile |
在模板类型推导的时候, 参数如果是数组或者函数名称, 他们会被退化成指针, 除非是用 在初始化引用类型 |
条款2: 理解auto 类型推导
auto类型推导与前面的模板推导基本相同,只存在一种情况不大相同。在一个用 auto 声明的变量上, 类型声明代替了 ParamType的作用, 所以也有三种情况:
- 情况1: 类型声明是一个指针或者是一个引用, 但不是一个通用的引用
- 情况2: 类型声明是一个通用引用
- 情况3: 类型声明既不是一个指针也不是一个引用
我们已经看了情况1和情况3的例子:
1 | auto x = 27; // 情况3( x既不是指针也不是引用) |
情况2则如下
1 | auto&& uref1 = x; // x是int并且是左值 |
条款1讲解了在非引用类型声明里, 数组和函数名称如何退化成指针。 这在 auto 类型推导上面也是一样:
1 | const char name[] = // name的类型是const char[13] |
正如你所见, auto 类型推导和模板类型推导工作很类似。
除了有一种情况是不一样的。 我们从如果你想声明一个用27初始化的 int 。
1 | int x1 = 27; |
综上四种语法, 都会生成一种结果: 一个拥有27数值的 int 。
但是若把int替换成auto,则会出现如下情况。
1 | auto x1 = 27; |
头两个的确是一样的, 声明一个初始化值为27的 int 。 然而后面两个, 声明了一个类型std::intializer_list
1 | auto x1 = 27; // 类型时int, 值是27 |
对待花括号初始化的行为是 auto 唯一和模板类型推导不一样的地方。 当 auto 声明变量被使用一对花括号初始化, 推导的类型是 std::intializer_list 的一个实例。 但是如果相同的初始化递给相同的模板, 类型推导会失败, 代码不能编译。
1 | auto x = { 11, 23, 9 }; // x的类型是 |
但是, 如果你明确模板的 param 的类型是一个不知道 T 类型的 std::initializer_list
1 | template<typename T> |
所以 auto 和模板类型推导的本质区别就是 auto 假设花括号初始化代表的是std::initializer_list, 但是模板类型推导却不是 。所以一个使用 auto 声明的返回值的函数, 返回一个花括号初始化就无法编译。
1 | auto createInitList() |
在C++14的lambda里面, 当 auto 用在参数类型声明的时候也是如此:
1 | std::vector<int> v; |
总结 |
---|
auto 类型推导通常和模板类型推导类似, 但是 auto 类型推导假定花括号初始化代表的 类型是 std::initializer_list , 但是模板类型推导却不是这样 |
auto 在函数返回值或者lambda参数里面执行模板的类型推导, 而不是通常意义 的 auto 类型推导 |
auto 关键字
条款3: 优先使用 auto 而非显式类型声明
由于 auto 使用类型推导( 参见条款2) , 它可以表示那些仅仅被编译器知晓的类型:
1 | auto dereUPLess = // comparison func. |
非常酷。 在 C++14 中, 模板( 原文为temperature) 被进一步丢弃, 因为使用 lambda 表达式的参数可以包含 auto :
1 | auto derefLess = // C++14 comparison |
auto 的优点除了可以避免未初始化的变量, 变量声明引起的歧义, 直接持有封装体的能力。还有一个就是可以避免“类型截断”问题。 下面有个例子, 你可能见过或者写过:
1 | std::vector<int> v; |
v.size() 定义的返回类型是 std::vector
1 | auto sz = v.size() // sz's type is std::vector<int>::size_type |
仍然不太确定使用 auto 的高明之处? 看看下面的代码:
1 | std::unordered_map<std::string, int> m; |
这看上去完美合理。 但是有一个问题, 你看出来了吗? 意识到 std::unorder_map 的 key 部分是 const 类型的, 在哈希表中的 std::pair 的类型不是 std::pair
1 | for (const auto& p : m) |
总结 |
---|
auto 变量一定要被初始化, 并且对由于类型不匹配引起的兼容和效率问题有免疫力, 可 以简单化代码重构, 一般会比显式的声明类型敲击更少的键盘 |
auto 类型的变量也受限于条款2和条款6中描述的陷阱 |
条款4:当auto推导出非预期类型时应当使用显式的类型初始化
条款5解释了使用 auto 关键字去声明变量, 这样就比直接显示声明类型提供了一系列的技术优势, 但是有时候 auto 的类型推导会和你想的南辕北辙。
举一个例子, 假设我有一个函数接受一个 Widget 返回一个 std::vector
1 | std::vector<bool> features(const Widget& w); |
进一步的, 假设第五个bit表示 Widget 是否有高优先级。 我们可以这样写代码:
1 | Widget w; |
这份代码没有任何问题。 它工作正常。 但是如果我们做一个看起来无伤大雅的修改,把 highPriority 的显式的类型换成 auto :
1 | auto highPriority = features(w)[5]; // w是不是个高优先级的? |
情况变了。 所有的代码还是可以编译, 但是他的行为变得不可预测:
1 | processWidget(w, highPriority); // 未定义行为 |
正如注释中所提到的, 调用 processWidget 现在会导致未定义的行为。 但是为什么呢? 答案是非常的令人惊讶的。 在使用 auto 的代码中, highPriority 的类型已经不是 bool 了。 尽管 std::vector
条款5:优先使用 nullptr 而不是 0 或者 NULL
在 C++98 中, 这意味着重载指针和整数类型的函数的行为会令人吃惊。 传递 0 或者 NULL 作为参数给重载函数永远不会调用指针重载的那个函数:
1 | void f(int); // 函数f的三个重载 |
使用 nullptr 作为参数去调用重载函数 f 将会调用 f(void*) 重载体, 因为 nullptr 不能被视为整数类型的:
1 | f(nullptr); //调用f(void*)重载体 |
另一方面, 你如果看到下面的代码:
1 | auto result = findRecord( /* arguments */); |
明显就没有歧义了: result 一定是个指针类型。
总结 |
---|
相较于 0 和 NULL , 优先使用 nullptr |
避免整数类型和指针类型之间的重载 |
优先使用声明别名而不是 typedef
1 | template<typname T> // MyAllocList<T> |
使用using 来进行别名声明。而若使用 typedef , 你不得不从草稿图开始去做一个蛋糕:
1 | template<typename T> // MyAllocList<T>::type |
如果你想在一个模板中使用 typedef 来完成创建一个节点类型可以被模板参数指定的链接表的任务, 你必须在 typedef 名称之前使用 typename
1 | template<typename T> // Widget<T> 包含 |
此处, MyAllocList
如果 MyAllocList 被定义为一个声明别名, 就不需要使用 typename
1 | template<typname T> |
对你来说, MyAllocList
总结 |
---|
typedef 不支持模板化, 但是别名声明支持 |
模板别名避免了 ::type 后缀, 在模板中, typedef 还经常要求使用 typename 前缀 |
C++14 为 C++11 中的类型特征转换提供了模板别名 |
优先使用作用域限制的eums而不是无作用域的 enum
一般而言, 在花括号里面声明的变量名会限制在括号外的可见性。 但是这对于 C++98 风格的 enums 中的枚举元素并不成立。 枚举元素和包含它的枚举类型同属一个作用域空间, 这意味着在这个作用域中不能再有同样名字的定义:
1 | enum Color { black, white, red}; // black, white, red 和 |
事实就是枚举元素泄露到包含它的枚举类型所在的作用域中, 对于这种类型的 enum 官方称作无作用域的( unscoped ) 。 在 C++11 中对应的使用作用域的enums( scoped enums ) 不会造成这种泄露:
1 | enum class Color { black, white, red}; // black, white, red |
因为限制作用域的 enum 是通过”enum class”来声明的, 它们有时被称作枚举类( enum class ) 。
并且无作用域的enum会存在隐性类型转换。
1 | enum Color { black, white, red }; // 无限制作用域的enum |
在 “enum” 后增加一个 “class” , 就可以将一个无作用域的 enum 转换为一个有作用域的 enum , 变成一个有作用域的 enum 之后, 事情就变得不一样了。 在有作用域的 enum 中不存在从枚举元素到其他类型的隐式转换:
1 | enum class Color { black, white, red }; // 有作用域的enum |
如果你就是想将 Color 类型转换为一个其他类型, 使用类型强制转换( cast ) 可以满足你这种变态的需求:
1 | if(static_cast<double>(c) < 14.5) { // 怪异但是有效的代码 |
相较于无定义域的 enum , 有定义域的 enum 也许还有第三个优势, 因为有定义域的 enum 可以被提前声明的, 即可以不指定枚举元素而进行声明:
1 | enum Color; // 出错! |
总结 |
---|
C++98 风格的 enum 是没有作用域的 enum |
有作用域的枚举体的枚举元素仅仅对枚举体内部可见。 只能通过类型转换( cast ) 转换 为其他类型 |
有作用域和没有作用域的 enum 都支持指定潜在类型。 有作用域的 enum 的默认潜在类型 是 int 。 没有作用域的 enum 没有默认的潜在类型。 |
有作用域的 enum 总是可以前置声明的。 没有作用域的 enum 只有当指定潜在类型时才可 以前置声明。 |
优化使用delete键字删除函数而不是private却又不实现的函数
使用delete关键字可让函数在重载里不会隐式调用已经被删除的函数,而若使用private可能会隐式的调用未实现的private函数。
当有一个函数,它在类的外边和内部都是是无法工作的, 当它工作时, 知道链接的时候可能又不工作了。 所以还是坚持使用删除函数吧。
总结 |
---|
优先使用删除函数而不是私有而不定义的函数 |
任何函数都可以被声明为删除, 包括非成员函数和模板实现 |
使用override关键字声明覆盖的函数
若有父类有虚函数,派生类可以使用override关键字进行覆盖,而若要使用override关键字进行覆盖则需要遵守以下规则 。
- 基类中的函数被声明为虚的。
- 基类中和派生出的函数必须是完全一样的( 出了虚析构函数) 。
- 基类中和派生出的函数的参数类型必须完全一样。
- 基类中和派生出的函数的常量特性必须完全一样。
- 基类中和派生出的函数的返回值类型和异常声明必须使兼容的。
- 函数的引用修饰符必须完全一样。 成员函数的引用修饰符是很少被提及的 C++11 的特性, 所以你之前没有听说过也不要惊奇。这些修饰符使得将这些函数只能被左值或者右值使用成为可能。
成员函数不需要声明为虚就可以使用它们:
1 | class Widget{ |
如果一个虚函数在基类中有一个引用修饰符, 派生类中对应的那个也必须要有完全一样的引用修饰符。 如果不完全一样, 派生类中的声明的那个函数也会存在, 但是它不会覆盖基类中的任何东西。
1 | class Base { |
以上代码因为未使用override,所以Derived 里面的mf1()与 Base里面的mf1()const 里面可能是重载关系。而若使用override关键字将会是真正的覆盖关系。
1 | class Derived: public Base { |
而这种方式是无法通过编译的,因为与Base的参数完全不同。