effective modern c++
条款一:类型推导
1.参数类型是指针或引用,但不是万能引用
忽略掉指针和引用符号,对类型进行对应,如果形参无const,根据实参有没有const决定推导类型有没有const,如果形参有const,无论实参有没有const推导类型都有const
2.参数类型是万能引用
万能引用即右值引用T&&
,如果实参是左值,则被推导成左值引用,注意即使实参不是引用也会被推到成左值引用,如果是右值,则为右值,是不是引用取决于实参。 保留const
3.参数类型既不是指针也不是引用
按值传递,直接推导类型,忽略const和volatile和引用。
数组的推导:由于数组和指针的混淆性,若不加引用号的话,数组会被推导成指针,若加了引用号,则会被推导成数组,并且可以获取数组大小,利用这一特性可以在编译期获取数组大小。
条款二:auto
auto的类型推导和函数参数的类型推导同理
但是当auto声明的变量是大括号括起时,推导类型属于initializer_list<T>
,特别注意只有auto能推导出大括号括起的初始化表达式,这也是auto和参数推导的唯一区别。
c++14支持auto推导函数返回值和auto推导lambda函数参数,这些时候auto适用于参数(模板)类型推导。
条款三:decltype
decltype的类型推导很简单,只会原原本本的返回其类型,保留const、引用等修饰。
decltype的主要用途:
返回值型别尾序语法:
1 | template<typename Container,typename Index> |
这里auto的之前的auto没有关系,只是说明这里用了返回值型别尾序语法,这个语法的作用是可以在返回值中用到函数形参。
c++11不支持auto作为函数返回值,而c++14支持,但是使用auto作为返回值经常会发生问题,例如将引用类型省略掉,这是非常致命的,可以用decltype(auto)
解决这个问题。
在c++14中上述代码可以写成
1 | template<typename Container,typename Index> |
这个代码还有改动的余地,因为参数只支持左值引用,可以用万能引用解决这个问题,但是要注意是注意使用万能引用要搭配完美转发
1 | //c++11 |
关于decltype还需要注意的一点是 int x=10 中,x是int类型,但是(x)是int&类型,需要注意
1 | int x=10; |
条款四:查看类型推导的方法
typeid(x).name()
boost::typeindex::type_id
条款五:优先选用auto,而非显式型别声明
auto优点
- 显式型别声明未初始化不会报错,但可能会导致未定义行为,auto未初始化会报错
- 使用auto可以表示复杂类型或编译器知道的类型,比如函数闭包(性能显著优于std::function)
- 避免错误的类型转换:从函数返回值到显式声明的类型有类型转换,这可能会造成问题
闭包:c++中闭包(Closure)通常指一个能捕获其所在作用域内变量的函数对象,比如lambda函数
条款六:当auto推导的型别不符合要求时,使用带显式型别的初始化物习惯用法
隐形代理:代理模式的应用,有些容器返回的不是基本类型,而是一种代理,比如
vector<bool>
返回的是std::vector<bool>::reference
类型
当有隐形代理时使用auto不会得到想要的类型,此时除了使用显式的类型声明,还可以使用带显式型别的初始化物习惯用法
1 | auto highPriority = static_cast<bool>(features(w)[5]) |
相较于显示类型声明的优点:更加直观的表现了类型转换。
条款七:在创建对象时注意()和{}
相较于小括号初始化,大括号初始化可应用的语境最广泛,可以阻止隐式窄化形型别转换,还对最令人苦恼的解析语法免疫。
如果要自己写使用大括号初始化的函数,要注意在构造函数重载期间,只要有任何可能,大括号初始化物就会与带有std::initializer_list
型别的形参相匹配,即使其他重载版本有更加匹配的形参表。
条款八:优先使用nullptr,而非0或NULL
0和NULL都不是指针类型,而nullptr可以视为指针,因为其可以隐式转换成所有指针类型。
在使用模板时,nullptr的优势展现出来,0和NULL会被类型推导为整形而非函数,这可能与某些函数的参数类型不符,从而报错,而nullptr则不会发生这种情况。
条款九:优先使用别名,而非typedef
using可以声明模板别名,而typedef不能
通过typedef声明的模板别名以type成员的形式出现,如果要在模板内使用时还必须加typename修饰
1 | template<typename T> |
c++型别转换:在
<type_traits>
中给出接口,在c++11中是通过嵌套在struct中的typedef实现的,使用时需调用std::remove_const<T>::type
,在c++14中又通过using实现了等效的接口std::remove_const_t<T>
条款十:优先选用限定作用域的枚举类型,而非不限作用域的枚举类型
1 | enum UserInfo{name,email,reputation};//不限作用域的枚举类型 |
两者的主要区别是
- 限定作用域的枚举类型其枚举变量名只在内部可见,可以降低名字空间污染,而不限定作用域的枚举类型其变量名不能在相同作用域下重复使用
- 不限定作用域的枚举类型可隐式转换到整数类型,进而可转换到浮点数类型,这可能会造成某些错误,而限定作用域的枚举类型的类型转换需要通过
static_cast<size_t>(c)
- 限定作用域的枚举类型总是可以前置声明,而不限定作用域的枚举类型必须在指定默认底层型别的前提下才能进行前置声明。
条款十一:优先选用删除函数,而非private未定义函数
如果你编写了某个函数,但是想阻止程序员调用某个特定函数的话,在C++98中,通常的方法是将其声明为private并不定义它们,这样就阻止了调用或者在链接阶段因为缺少函数定义而失败。
在c++11中使用=delete
可以实现同样的效果,而且98的方法只能针对成员函数,并且无法阻止特化模板函数(因为其无法被声明为private),11的做法可以删除任何函数。
条款十二:为意在改写的函数添加override声明
关于重载重写(覆盖)和隐藏:
(1)函数重载发生在相同作用域;
(2)函数隐藏发生在不同作用域;
(3)函数覆盖就是函数重写。准确地叫作虚函数覆盖和虚函数重写,也是函数隐藏的特例。
关于三者的对比,李健老师在《编写高质量代码:改善C++程序的150个建议》给出了较为详细的总结,如下表所示:
三者 作用域 有无virtual 函数名 形参列表 返回值类型 重载 相同 可有可无 相同 不同 可同可不同 隐藏 不同 可有可无 相同 可同可不同 可同可不同 重写 不同 有 相同 相同 相同(协变)
为什么要加override?因为函数重载重写的混淆性,很多时候想要实现重写但由于某些错误没有实现,但是这时编译器不会报错,反而会顺利编译通过,无法达到我们想要的效果。在派生类中加上override以后,编译器就会吹毛求疵的检查函数是否为重写,
再详细的补充一下函数重写的规则
- 基类中的函数是虚函数
- 基类和派生类中的函数名必须完全相同
- 基类和派生类中函数形参型别必须完全想让
- 基类和派生类的常量性必须相同(constness)
- 基类和派生类的返回值必须相同或协变(即派生类返回的类型是基类返回类型的子类型)
c++11的新规则还规定
- 基类和子类的函数引用饰词相同
关于函数引用饰词:用于表示调用这个函数的引用类型应该是左值还是右值
1
2
3
4
5 class Widget{
public:
void dowork() &;//这个版本仅在*this是左值时使用
void dowork() &&;//这个版本仅在*this是右值时使用
}引用修饰符在括号里面表示参数的引用类型,在函数后面表示调用者的引用类型。