《Effective Modern C++》笔记
- 条款一:理解模板类型推导
- 条款二:理解auto类型推导
- 条款三:理解decltype
- 条款四:学会查看类型推导结果
- 条款五:优先考虑auto而非显式类型声明
- 条款六:auto推导若非己愿,使用显式类型初始化惯用法
- 条款七:区别使用()和{}创建对象
- 条款八:优先考虑nullptr而非0和NULL
- 条款九:优先考虑别名声明而非typedefs
- 条款十:优先考虑限域enum而非未限域enum
- 条款十一:优先考虑使用deleted函数而非使用未定义的私有声明
- 条款十二:使用override声明重写函数
- 条款十三:优先考虑const_iterator而非iterator
- 条款十四:如果函数不抛出异常请使用noexcept
- 条款十五:尽可能的使用constexpr
- 条款十六:让const成员函数线程安全
- 条款十七:理解特殊成员函数的生成
- 条款十八:对于独占资源使用std::unique_ptr
- 条款十九:对于共享资源使用std::shared_ptr
- 条款二十:当std::shard_ptr可能悬空时使用std::weak_ptr
- 条款二十一:优先考虑使用std::make_unique和std::make_shared,而非直接使用new
- 条款二十二:当使用Pimpl惯用法,请在实现文件中定义特殊成员函数
- 条款二十三:理解std::move和std::forward
- 条款二十四:区分通用引用与右值引用
- 条款二十五:对右值引用使用std::move,对通用引用使用std::forward
- 条款二十六:避免在通用引用上重载
- 条款二十七:熟悉通用引用重载的替代方法
- 条款二十八:理解引用折叠
- 条款二十九:认识移动操作的缺点
- 条款三十:熟悉完美转发失败的情况
- 条款三十一:避免使用默认捕获模式
- 条款三十二:使用初始化捕获来移动对象到闭包中
- 条款三十三:对auto&&形参使用decltype以std::forward它们
- 条款三十四:考虑lambda而非std::bind
- 条款三十五:优先考虑基于任务的编程而非基于线程的编程
- 条款三十六:如果有异步的必要请指定std::launch::async
- 条款三十七:使std::thread在所有路径最后都不可结合
- 条款三十八:关注不同线程句柄的析构行为
- 条款三十九:对于一次性事件通信考虑使用void的futures
- 条款四十:对于并发使用std::atomic,对于特殊内存使用volatile
- 条款四十一:对于移动成本低且总是被拷贝的可拷贝形参,考虑按值传递
- 条款四十二:考虑使用置入代替插入
条款一:理解模板类型推导
请记住:
- 在模板类型推导时,有引用的实参会被视为无引用,他们的引用会被忽略
- 对于通用引用的推导,左值实参会被特殊对待
- 对于传值类型推导,const和/或volatile实参会被认为是non-const的和non-volatile的
- 在模板类型推导时,数组名或者函数名实参会退化为指针,除非它们被用于初始化引用
条款二:理解auto类型推导
请记住:
- auto类型推导通常和模板类型推导相同,但是auto类型推导假定花括号初始化代表std::initializer_list,而模板类型推导不这样做
- 在C++14中auto允许出现在函数返回值或者lambda函数形参中,但是它的工作机制是模板类型推导那一套方案,而不是auto类型推导
条款三:理解decltype
请记住:
- decltype总是不加修改的产生变量或者表达式的类型。
- 对于T类型的不是单纯的变量名的左值表达式,decltype总是产出T的引用即T&。
- C++14支持decltype(auto),就像auto一样,推导出类型,但是它使用decltype的规则进行推导。
条款四:学会查看类型推导结果
请记住:
- 类型推断可以从IDE看出,从编译器报错看出,从Boost TypeIndex库的使用看出
- 这些工具可能既不准确也无帮助,所以理解C++类型推导规则才是最重要的
条款五:优先考虑auto而非显式类型声明
请记住:
- auto变量必须初始化,通常它可以避免一些移植性和效率性的问题,也使得重构更方便,还能让你少打几个字。
- 正如Item2和6讨论的,auto类型的变量可能会踩到一些陷阱。
条款六:auto推导若非己愿,使用显式类型初始化惯用法
请记住:
- 不可见的代理类可能会使auto从表达式中推导出“错误的”类型
- 显式类型初始器惯用法强制auto推导出你想要的结果
条款七:区别使用()和{}创建对象
请记住:
- 括号初始化是最广泛使用的初始化语法,它防止变窄转换,并且对于C++最令人头疼的解析(实例化对象调用默认构造函数:Widget w();)有天生的免疫性
- 在构造函数重载决议中,括号初始化尽最大可能与std::initializer_list参数匹配,即便其他构造函数看起来是更好的选择
- 对于数值类型的std::vector来说使用花括号初始化和小括号初始化会造成巨大的不同
- 在模板类选择使用小括号初始化或使用花括号初始化创建对象是一个挑战。
条款八:优先考虑nullptr而非0和NULL
请记住
- 优先考虑nullptr而非0和NULL
- 避免重载指针和整型
条款九:优先考虑别名声明而非typedefs
别名声明可以被模板化(这种情况下称为别名模板alias templates)但是typedef不能,例子:
使用别名模板:
template<typename T> //MyAllocList<T>是
using MyAllocList = std::list<T, MyAlloc<T>>; //std::list<T, MyAlloc<T>>
//的同义词
MyAllocList<Widget> lw; //用户代码
使用typedef:
template<typename T> //MyAllocList<T>是
struct MyAllocList { //std::list<T, MyAlloc<T>>
typedef std::list<T, MyAlloc<T>> type; //的同义词
};
MyAllocList<Widget>::type lw; //用户代码
请记住:
- typedef不支持模板化,但是别名声明支持。
- 别名模板避免了使用“::type”后缀,而且在模板中使用typedef还需要在前面加上typename
- C++14提供了C++11所有type traits转换的别名声明版本
条款十:优先考虑限域enum而非未限域enum
未限域枚举(unscoped enum):enum Color{}
限域枚举(scoped enum):enum class Color{}
限域枚举(枚举类)的优点:
- 它不会导致枚举名泄漏
- 在它的作用域中,枚举名是强类型。未限域enum中的枚举名会隐式转换为整型(现在,也可以转换为浮点类型)
- 限域enum可以被前置声明。也就是说,它们可以不指定枚举名直接声明。不能前置enum声明的最大的缺点莫过于它可能增加编译依赖。
请记住
- C++98的enum即非限域enum。
- 限域enum的枚举名仅在enum内可见。要转换为其它类型只能使用cast。
- 非限域/限域enum都支持底层类型说明语法,限域enum底层类型默认是int。非限域enum没有默认底层类型。
- 限域enum总是可以前置声明。非限域enum仅当指定它们的底层类型时才能前置。
条款十一:优先考虑使用deleted函数而非使用未定义的私有声明
请记住:
- 比起声明函数为private但不定义,使用deleted函数更好
- 任何函数都能被删除(be deleted),包括非成员函数和模板实例(译注:实例化的函数)
条款十二:使用override声明重写函数
请记住:
- 为重写函数加上override
- 成员函数引用限定让我们可以区别对待左值对象和右值对象(即*this)
条款十三:优先考虑const_iterator而非iterator
请记住:
- 优先考虑const_iterator而非iterator
- 在最大程度通用的代码中,优先考虑非成员函数版本的begin,end,rbegin等,而非同名成员函数
条款十四:如果函数不抛出异常请使用noexcept
给不抛异常的函数加上noexcept的动机:它允许编译器生成更好的目标代码
请记住:
- noexcept是函数接口的一部分,这意味着调用者可能会依赖它
- noexcept函数较之于non-noexcept函数更容易优化
- noexcept对于移动语义,swap,内存释放函数和析构函数非常有用
- 大多数函数是异常中立的(译注:可能抛也可能不抛异常)而不是noexcept
条款十五:尽可能的使用constexpr
所有constexpr对象都是const,但不是所有const对象都是constexpr。因为constexpr在修饰函数的时候,有特性,举个例子:
constexpr //pow是绝不抛异常的
int pow(int base, int exp) noexcept //constexpr函数
{
… //实现在下面
}
constexpr auto numConds = 5; //(上面例子中)条件的个数
std::array<int, pow(3, numConds)> results; //结果有3^numConds个元素
C++11中,constexpr函数的代码不超过一行语句:一个return。
在C++14中,constexpr函数的限制变得非常宽松了,不限制行数。
请记住:
- constexpr对象是const,它被在编译期可知的值初始化
- 当传递编译期可知的值时,constexpr函数可以产出编译期可知的结果
- constexpr对象和函数可以使用的范围比non-constexpr对象和函数要大
- constexpr是对象和函数接口的一部分
条款十六:让const成员函数线程安全
请记住:
- 确保const成员函数线程安全,除非你确定它们永远不会在并发上下文(concurrent context)中使用。
- 使用std::atomic变量可能比互斥量提供更好的性能,但是它只适合操作单个变量或内存位置。
条款十七:理解特殊成员函数的生成
请记住:
- 特殊成员函数是编译器可能自动生成的函数:默认构造函数,析构函数,拷贝操作,移动操作。
- 移动操作仅当类没有显式声明移动操作,拷贝操作,析构函数时才自动生成。
- 拷贝构造函数仅当类没有显式声明拷贝构造函数时才自动生成,并且如果用户声明了移动操作,拷贝构造就是delete。拷贝赋值运算符仅当类没有显式声明拷贝赋值运算符时才自动生成,并且如果用户声明了移动操作,拷贝赋值运算符就是delete。当用户声明了析构函数,拷贝操作的自动生成已被废弃。
- 成员函数模板不抑制特殊成员函数的生成。
条款十八:对于独占资源使用std::unique_ptr
请记住:
- std::unique_ptr是轻量级、快速的、只可移动(move-only)的管理专有所有权语义资源的智能指针
- 默认情况,资源销毁通过delete实现,但是支持自定义删除器。有状态的删除器和函数指针会增加std::unique_ptr对象的大小
- 将std::unique_ptr转化为std::shared_ptr非常简单
条款十九:对于共享资源使用std::shared_ptr
如果你想创建一个用std::shared_ptr管理的类,这个类能够用this指针安全地创建一个std::shared_ptr,std::enable_shared_from_this就可作为基类的模板类。
请记住:
- std::shared_ptr为有共享所有权的任意资源提供一种自动垃圾回收的便捷方式。
- 较之于std::unique_ptr,std::shared_ptr对象通常大两倍,控制块会产生开销,需要原子性的引用计数修改操作。
- 默认资源销毁是通过delete,但是也支持自定义删除器。删除器的类型是什么对于std::shared_ptr的类型没有影响。
- 避免从原始指针变量上创建std::shared_ptr。
条款二十:当std::shard_ptr可能悬空时使用std::weak_ptr
请记住:
- 用std::weak_ptr替代可能会悬空的std::shared_ptr。
- std::weak_ptr的潜在使用场景包括:缓存、观察者列表、打破std::shared_ptr环状结构。
条款二十一:优先考虑使用std::make_unique和std::make_shared,而非直接使用new
请记住:
- 和直接使用new相比,make函数消除了代码重复,提高了异常安全性。对于std::make_shared和std::allocate_shared,生成的代码更小更快。
- 不适合使用make函数的情况包括需要指定自定义删除器和希望用花括号初始化。
- 对于std::shared_ptrs,其他不建议使用make函数的情况包括(1)有自定义内存管理的类;(2)特别关注内存的系统,非常大的对象,以及std::weak_ptrs比对应的std::shared_ptrs活得更久。
条款二十二:当使用Pimpl惯用法,请在实现文件中定义特殊成员函数
请记住:
- Pimpl惯用法通过减少在类实现和类使用者之间的编译依赖来减少编译时间。
- 对于std::unique_ptr类型的pImpl指针,需要在头文件的类里声明特殊的成员函数,但是在实现文件里面来实现他们。即使是编译器自动生成的代码可以工作,也要这么做。
- 以上的建议只适用于std::unique_ptr,不适用于std::shared_ptr。
条款二十三:理解std::move和std::forward
lvalue-reference-to-const允许被绑定到一个const右值上
std::move:第一,不要在你希望能移动对象的时候,声明他们为const。对const对象的移动请求会悄无声息的被转化为拷贝操作。第二点,std::move不仅不移动任何东西,而且它也不保证它执行转换的对象可以被移动。
std::forward是一个有条件的转换:它的实参用右值初始化时,转换为一个右值。
请记住:
- std::move执行到右值的无条件的转换,但就自身而言,它不移动任何东西。
- std::forward只有当它的参数被绑定到一个右值时,才将参数转换为右值。
- std::move和std::forward在运行期什么也不做。
条款二十四:区分通用引用与右值引用
请记住:
- 如果一个函数模板形参的类型为T&&,并且T需要被推导得知,或者如果一个对象被声明为auto&&,这个形参或者对象就是一个通用引用。
- 如果类型声明的形式不是标准的type&&,或者如果类型推导没有发生,那么type&&代表一个右值引用。
- 通用引用,如果它被右值初始化,就会对应地成为右值引用;如果它被左值初始化,就会成为左值引用。
条款二十五:对右值引用使用std::move,对通用引用使用std::forward
请记住:
- 最后一次使用时,在右值引用上使用std::move,在通用引用上使用std::forward。
- 对按值返回的函数要返回的右值引用和通用引用,执行相同的操作。
- 如果局部对象可以被返回值优化消除,就绝不使用std::move或者std::forward。
条款二十六:避免在通用引用上重载
请记住:
- 对通用引用形参的函数进行重载,通用引用函数的调用机会几乎总会比你期望的多得多。
- 完美转发构造函数是糟糕的实现,因为对于non-const左值,它们比拷贝构造函数而更匹配,而且会劫持派生类对于基类的拷贝和移动构造函数的调用。
条款二十七:熟悉通用引用重载的替代方法
请记住:
- 通用引用和重载的组合替代方案包括使用不同的函数名,通过lvalue-reference-to-const传递形参,按值传递形参,使用tag dispatch。
- 通过std::enable_if约束模板,允许组合通用引用和重载使用,但它也控制了编译器在哪种条件下才使用通用引用重载。
- 通用引用参数通常具有高效率的优势,但是可用性就值得斟酌。
条款二十八:理解引用折叠
如果任一引用为左值引用,则结果为左值引用。否则(即,如果引用都是右值引用),结果为右值引用。
请记住:
- 引用折叠发生在四种情况下:模板实例化,auto类型推导,typedef与别名声明的创建和使用,decltype。
- 当编译器在引用折叠环境中生成了引用的引用时,结果就是单个引用。有左值引用折叠结果就是左值引用,否则就是右值引用。
- 通用引用就是在特定上下文的右值引用,上下文是通过类型推导区分左值还是右值,并且发生引用折叠的那些地方。
条款二十九:认识移动操作的缺点
请记住:
- 假定移动操作不存在,成本高,未被使用。
- 在已知的类型或者支持移动语义的代码中,就不需要上面的假设。
条款三十:熟悉完美转发失败的情况
请记住:
- 当模板类型推导失败或者推导出错误类型,完美转发会失败。
- 导致完美转发失败的实参种类有花括号初始化,作为空指针的0或者NULL,仅有声明的整型static const数据成员,模板和重载函数的名字,位域。
条款三十一:避免使用默认捕获模式
请记住:
- 默认的按引用捕获可能会导致悬空引用。
- 默认的按值捕获对于悬空指针很敏感(尤其是this指针),并且它会误导人产生lambda是独立的想法。
条款三十二:使用初始化捕获来移动对象到闭包中
请记住:
- 使用C++14的初始化捕获将对象移动到闭包中(C++14可以捕获表达式的结果)。
- 在C++11中,通过手写类或std::bind的方式来模拟初始化捕获。
条款三十三:对auto&&形参使用decltype以std::forward它们
lambda表达式完美转发
请记住:
- 对auto&&形参使用decltype以std::forward它们
条款三十四:考虑lambda而非std::bind
请记住:
与使用std::bind相比,lambda更易读,更具表达力并且可能更高效。
只有在C++11中,std::bind可能对实现移动捕获或绑定带有模板化函数调用运算符的对象时会很有用。
条款三十五:优先考虑基于任务的编程而非基于线程的编程
请记住:
- std::thread API不能直接访问异步执行的结果,如果执行函数有异常抛出,代码会终止执行。
- 基于线程的编程方式需要手动的线程耗尽、资源超额、负责均衡、平台适配性管理。
- 通过带有默认启动策略的std::async进行基于任务的编程方式会解决大部分问题。
条款三十六:如果有异步的必要请指定std::launch::async
请记住:
- std::async的默认启动策略是异步和同步执行兼有的。
- 这个灵活性导致访问thread_locals的不确定性,隐含了任务可能不会被执行的意思,会影响调用基于超时的wait的程序逻辑。
- 如果异步执行任务非常关键,则指定std::launch::async。
条款三十七:使std::thread在所有路径最后都不可结合
请记住:
- 在所有路径上保证thread最终是不可结合的。
- 析构时join会导致难以调试的表现异常问题。
- 析构时detach会导致难以调试的未定义行为。
- 声明类数据成员时,最后声明std::thread对象。
条款三十八:关注不同线程句柄的析构行为
请记住:
- future的正常析构行为就是销毁future本身的数据成员。
- 引用了共享状态——使用std::async启动的未延迟任务建立的那个——的最后一个future的析构函数会阻塞住,直到任务完成。
条款三十九:对于一次性事件通信考虑使用void的futures
请记住:
- 对于简单的事件通信,基于条件变量的设计需要一个多余的互斥锁,对检测和反应任务的相对进度有约束,并且需要反应任务来验证事件是否已发生。
- 基于flag的设计避免的上一条的问题,但是是基于轮询,而不是阻塞。
- 条件变量和flag可以组合使用,但是产生的通信机制很不自然。
- 使用std::promise和future的方案避开了这些问题,但是这个方法使用了堆内存存储共享状态,同时有只能使用一次通信的限制。
条款四十:对于并发使用std::atomic,对于特殊内存使用volatile
请记住:
- std::atomic用于在不使用互斥锁情况下,来使变量被多个线程访问的情况。是用来编写并发程序的一个工具。
- volatile用在读取和写入不应被优化掉的内存上。是用来处理特殊内存的一个工具。
条款四十一:对于移动成本低且总是被拷贝的可拷贝形参,考虑按值传递
请记住:
- 对于可拷贝,移动开销低,而且无条件被拷贝的形参,按值传递效率基本与按引用传递效率一致,而且易于实现,还生成更少的目标代码。
- 通过构造拷贝形参可能比通过赋值拷贝形参开销大的多。
- 按值传递会引起切片问题,所说不适合基类形参类型。
条款四十二:考虑使用置入代替插入
解释了emplace_back为什么比push_back快。
请记住:
- 原则上,置入函数有时会比插入函数高效,并且不会更差。
- 实际上,当以下条件满足时,置入函数更快:(1)值被构造到容器中,而不是直接赋值;(2)传入的类型与容器的元素类型不一致;(3)容器不拒绝已经存在的重复值。
- 置入函数可能执行插入函数拒绝的类型转换。