每天学习亿点点: 7.6 Effective Modern C++

2021.7.19 更新 全部笔记上传完毕

 

1.理解类型推断

parameter type 的不同情况下:

传进来的称为expr

1.是指针或非通用引用

expr的reference部分忽略掉

2.通用引用
唯一T&& T和parameter推断成引用

expr是右值的话则适用于1的情况

比如:T&& 传入int rvalue时则变为T 推断为int

3.既不是指针也不是引用

就是pass by value

传入参数的引用部分忽略

T本身的const和volatile丢失,因为copy一份了,const和volatile都失去了意义

4. 传入参数是数组或者函数名时

T不是引用则退化为指针

如果是引用则T就是引用

例如:

template<typename T, std::size_t N>

constexpr std::size_t arraySize(T (&)[N]) noexcept

{

return N;
}

直接得到数组的长度

 

2.理解auto type推断

auto的类型检测和模板里的类型检测几乎是一样,除了:

auto假设{V1,V2,...}是std::initializer_list<T>

而且如果V1,V2等参数的类型不同还无法完成类型推断

return或者lambda参数里用auto的时候是用的模板类类型推断的规则不是auto类型推断的规则,要注意,也就是这里不能用{v1,v2,v3..}这种初始化了

 

3.理解decltype

auto f()->decltype(xxx)

是C++11的一种延后推断type的方式

在C++14里取消了decltype的配对使用,即只需要使用auto即可

 

1.decltype几乎都是原原本本报告类型

2.Type T的lvalue 表达式例如:

(T), decltype得到的结果是T& 而不是 T, 一对括号就能改变变量的引用的存在

3. decltype(auto)和auto很类似,从初始化里推断类型,但是用的确实decltype的规则

 

4.了解如何查看推导的类型

类型推断查看:

源码时期IDE,编译时期Compiler error,运行时期Boost TypeIndex library输出

这些玩意都不靠谱

 

5.多用auto而不是显示类型声明

typename std::iterator_traits<It>::value_type

描述某个iterator类型的时候可以用这种书写方法

 

std::function object用更多的内存也更慢

 

auto好处:避免未初始化变量, 冗余的变量声明, 直接能hold住闭包,还可以避免类型捷径

unordered_map里面的第一个类型是const

坏处:2款提到过的{}类型推断以及在lambda表达式里的问题

 

不怕类型匹配失败,不怕refactoring,也打更少的字.

 

6.auto推断出不需要的类型时, 使用显示类型初始化

1.auto推断出你不太想要的类型主要还是看你对某个库熟不熟悉,返回的这种问题大多是由proxy类引起的,因为proxy类里才包含真正的类型数据,不直接把数据给auto的变量时候,因为proxy的临时变量会在copy给auto的变量之后面临销毁,所以之前的proxy临时变量的copy就变得没用了,之后对auto变量的操作就会变成未定义行为.

2.显示类型初始声明强制auto推断出你想要的类型

3.除了bool type都返回T&

bool type返回std::vector<bool>::reference type的 object

强转只会转成bool而不是bool&

 

7.在创建对象时区分()和{}

1.不会出现

Widget w2();这种初始零参构造结果被声明为函数的囧境.

2.各种情景都可以用

3.防止隐式收缩转换

4.空{}被看作是无参数而不是空list{}

5.再构造重载时,{}总是被最先和std::initializer_list参数匹配,即使其他的构造函数也能提供更好的匹配

6.vector<numeric type>{x,y}

vector<numeric type>(x,y)区别就很大

7.模板内部object的创建时选用括号和大括号是很麻烦的一件事

 

8.多用nullptr 而不是0或者NULL

nullptr和NULL,0不同是一个可隐式转换为所有类型的指针类型所以比实际上的类型为int,long的NULL和0更有区分度而且避免在int和类型指针上重载.

 

9.优先使用别名声明而不是类型定义

模板类型重命名还得先定义一个struct

如果要在类里使用这种类型还得模板类里前加上typename

1.typedef不是很好地支持模板化

但是别名声明却很好地支持

2.别名声明避免了::type后缀, 在模板里typename经常要在typedef前面使用

3.C++ 14为所有C++ 11类型特征转换提供了别名模板

 

10.多用带有定义域的枚举而不是无定义域的

1.template<typename E>

constexpr auto

toUType(E enumerator) noexcept

{

return static_cast<std::underlying_type_t<E>>(enumerator);

}

强行要用scoped enum去转到underlying type的时候就用上面这个函数

2.C++ 98风格的enum是unscoped的

3.class enumerator只在类内部可见, 只能用cast去转成其他类

4.unscoped和scoped枚举都支持潜在类型声明,scoped enum的潜在类型是int,unscoped类型无默认潜在类型

5.scoped enums可前向声明,unscoped enums 只有在指定了潜在类型之后才能前向声明.

 

11.多用deleted函数,而不是private 未定义函数

把delete函数声明为public的另一个好处是error message 会更好,因为声明为priate只会让编译器抱怨privateness

1. delete 包括非成员函数和模板实例化都可以使用

2. 私有化函数在类外不能用(private是类的权限级别),类内也不一定总是有用(成员函数模板实例化并放在private里面,这里就会出现特化模板和模板权限不一样报错),而且不到链接期间也不能用.

 

12.要把覆盖函数声明为override

1.成员函数重载时遵守的规则:

基函数必定是虚拟的

名字必须一致

参数类型一致

constness必须一致

返回类型和异常声明也必须保持一致

C++11还追加了对函数引用标签的限制

2.

void dowork()& 当*this是左值

void dowork()&&当*this是右值

使得左值和右值object调用同一个函数的结果不同

3.override关键字的主要一个好处是他能在编译诊断的时候告诉你有效信息,但是平常的override手段则不能

 

13.多用const_iterator 而不是iterator

1.C++98不太支持const_iterator

2.const_iterator使用之后不会修改指向的数据

3.在最大程度上要求泛型的代码里,最好是用非成员的begin,end,rbegin,cbegin而不是成员函数,因为有的第三方库或者老的container实现里可能并不包含某些我们希望的实现.

 

14.不抛出异常的函数就好好声明为noexcept

1.throw()只会直接解除调用栈绑定,回到f的调用者这里,这个时候如果你需要runtime的信息就需要自己追踪,然而抛出noexcept有可能并不会直接回到f调用者,这个时候就不用track runtime的信息了,就有了优化的空间了

2.wide contract无前置条件

narrow 有

所以wide一般有noexcept

narrow一般是没有因为前置条件有可能被违反,这个时候就必须要抛出异常了

3.noexcept(可以嵌套依赖其他的noexcept)

4.noexcept是函数接口的一部分,宣称调用者可以依赖他们

5.noexcept在move,swap,memory deallocation,destructor这里都是很有用的

6.大多数是exception-neutral的,本身不抛出异常,但是里面调用的其他函数可能抛出

 

15.只要有机会就要多用constexpr

1.constexpr是const然后必须被编译阶段就能知道的值初始化

2.constexpr函数如果在被调用的时候赋予的参数是编译期就能知道的值的话,能产生编译期结果

3.constexpr可使用的范围比非constexpr要广阔的多

4.constexpr是一个object或者函数的接口的一部分

 

16.把const成员函数做的线程安全点

std::mutex是move-only type所以加了它作为成员变量会导致类实例无法被copy

1.const 成员函数必须要是线程安全的,否则就不要用于并发情况

2.std::atomic比mutex有更好的性能,但是他们更适合用于一个变量或者内存空间

 

17.了解特殊成员函数的生成

cotr,des,copy cotr,copy assignment,public and inline,nonvirtual

 

C++ 11 add :move cotr, move assignment

1.默认构造函数当且仅当类中无用户自定义构造函数时才会生成

2.默认析构函数规则同1,但是它时默认noexcept的,并且只有在基类析构函数为虚的时候它才可能为虚

3.默认拷贝构造函数

如果没有用户定义默认拷贝构造函数

会被删除如果定义了move operation,如果有了默认赋值函数或者析构函数,这个默认的构造函数就会被弃用

4.默认赋值函数

如果没有用户定义默认赋值函数

会被删除如果定义了move operation,如果有了默认构造函数或者析构函数,这个默认的赋值函数就会被弃用

5.默认move函数包含构造和赋值

都是对非static member进行move操作

当且仅当没有用户自定义的copy operation or move operation or 析构函数

6.=default 关键字其实就覆盖了默认生成的成员函数

7.成员模板函数永远不会抑制默认生成的特殊函数

 

18. 独占内存管理时多用std::unique 

1.小巧,快速,只能move的管理独占资源的智能指针.

2.自定义delete可以取代默认destruction,但是带状态的deleter和函数指针会增加std::unique_ptr的大小很烦人

3.转化为std::shared_ptr很简单

 

19.共享内存管理时多用std::shared

The Curiously Recurring Template

Pattern (CRTP).

1.shared_ptr支持自动垃圾回收,引用计数

2.就是块头比较大,需要control block的资源消耗,还需要原子引用的操作

3.deleter的引用不会对shared_ptr的类型有影响因为它是存在control block里面的,实际上适合智能指针本身分开的

4.避免从raw指针类型创建shared_ptr,因为每次使用此方法创建必然会创建control block,这个就有点多余了

5.make_shared必然创建control block, 从std::unique_ptr or std::auto_ptr创建来必然会新建control block

 

20.将std::weak_ptr用于std::shared_ptr类可以悬挂的指针

1.使用weak_ptr给shared_ptr指针去保持dangle检测

2.weak_ptr.lock()能得到shared_ptr如果过期了就会得到空

3.使用场景主要是缓存,观察者列表,以及shared_ptr循环

 

21.相比于直接用new多用std::make_unique 和 std::make_shared

make_ptr(引用计数&weak 引用计数:管control block的内存是否释放)

new 方法:

引用计数管object,weak引用管control block 内存

expire函数是检查引用计数是不是0,是的话本weak_ptr就expire了weak_ref就减1.

 

1. make 函数减少代码重复,提高异常抛出安全性,并且生成代码更小更快

2.需要传入定制deleter和传入大括号初始列表时不宜使用

3.不宜使用的场合:当一个类有自己的内存管理机制

有内存管理隐患的系统,尤其是那些有非常大的object的,因为std::weak_ptr要活的长,而且这个时候销毁内存会非常滞后

 

22.当使用Pointer to implementation时, 在实现文件里定义特殊成员函数

先不提某些类型以减少include某些类型,编译时间减少,而且编译依赖变少,代码也不用变

1. pimpl idiom 减少buid时间,通过减少类定义和类实现之间的编译依赖

2. 对于unique_ptr类型, 在头文件里声明特殊成员函数,在实现文件里实现函数. 即使default也可以实现功能,但是还是要自己写这样能避免incomplete type 错误. 只要你的函数用到了某个类型,你就必须要在这个函数的定义之前确认某个类型的具体定义

3. 上面的建议只适用于unique_ptr不是shared_ptr

 

23. 理解std::move和std::forward

替代昂贵的复制和赋值构造函数

接收参数并把参数类型如实完美转发

右值引用你引用的是常量那我还怎么move啊

1.std::move 无脑cast成右值,并不移动任何东西

2.std::forward 把参数cast成一个右值, 当且仅当参数已经被绑定到一个右值上

3.两个函数在运行时间都不做任何事情

 

24.区分通用引用和右值引用

1.如果一个模板参数类型T&&或者是auto &&,并且这个类型需要被推断出来那么就会被认为是通用引用

2.如果类型声明不是严格的type &&或者类型推断并不发生,type&&也就只是个右值引用

3.通用指针初始化是啥,它就是啥,这个主要是引用崩塌完成的

 

25.在右值引用上使用std::move,在通用引用上使用std::forward

1.move应用到rv,forward用到ur

2.对rv和ur进行同样的处理,如果他们被值返回的函数返回了

3.不要在即将要进行RVO的local object上运用move或者forward,因为他们会让这个RVO失效

 

26.避免对通用引用进行重载

普通函数匹配要优于模板匹配

1.重载ur太霸道,因为重载他的频次太高了, 很容易就出现你不太想要的情况

2.完美转发非常有问题, 因为他们基本上是要比copy 构造函数(non-const lvalue)这样的调用要更容易被invoked,而且会拦截派生类调用基类copy和move 构造函数

 

27.熟悉通用引用重载的替代方法

解决通用引用过于贪婪的问题方法:

1. 使用不同的函数名字,

2. 传指向const的左值引用

3. 传值传参

4. 使用tag dispatch

std::enable_if是用来控制在某个条件下是否使用泛化模板

通用指针一般是由效率优势的,但是他们太贪婪了,不太好驾驭

 

28.理解引用崩塌reference collapsing

1. 模板实例化,auto 类型诊断, 创建和使用typedef, 和alias 声明,and decltype 这几种情况会出现引用析构

2. T&& &变成T& 其他的都rvalue

其实就是引用符号2个2个删除最后是啥就是啥

3. 通用引用是右值引用,在这个环境下类型诊断会把左值和右值区分开来此时也会发生引用collapsing

 

29.假设移动操作不存在,不便宜,不使用

实例不提供move operation的时候,强行用就会变成copy

 

move operation 也不见得就比copy快

 

需要move不抛出异常,但是你写的operation并没有声明为noexcept

 

很少情况下,只有右值能作为move operation的操作值

 

当类型都已知,而且支持move 语句,那就没必要这么认为了,大胆地用

 

30.多了解完美的forwarding的失败案例

1.完美转发会在模板类型诊断失败或者诊断出错误类型时失败

2.会导致参数错误的传入类型有:

大括号初始列表, 以0和NULL表示的空指针, 只声明了的const-static 数据成员, 模板和重载的函数名,以及位域(位域不是指针能指向的类型,C++种能指向的最小单位时char).

 

31.避免使用默认的捕捉模式

编译时期: lambda,closure class

运行时期:closure object

capture mode决定是否持有捕捉的数据的拷贝或者引用

capture只针对非静态变量(包括参数),而且这些变量是和lambda表达式同时创建的同一个scope才行.

1.capture只针对非静态变量(包括参数),而且这些变量是和lambda表达式同时创建的同一个scope才行.

2.引用捕捉会导致野引用

3.默认的值捕捉容易导致野指针(尤其是this指针), 而且会误导性地让读者认为lambda是独立的即可自行算出一切不需要外界的数据

 

32.使用init capture将对象移动到闭包中

1.C++ 14 可以使用init capture 把object move到闭包里面.

auto func = [pw = std::make_unique<Widget>()] // as before,

{ return pw->isValidated() // create pw

&& pw->isArchived(); }; // in closure

左边的pw是这个closure的数据成员不是全局的,右边才可以使用全局或者local variable

2.C++11不支持直接的init capture功能

但我们可以使用手写类和std::bind来模拟

auto func = std::bind( [](const std::unique_ptr<Widget>& pw) { return pw->isValidated() && pw->isArchived(); },

std::make_unique<Widget>()

);

 

33.在auto&&参数上使用decltype来std::forward它们

使用decltype 以下声明auto&& parameter的paramete来完美转发他们

 

34.多用lambda而不是std::bind

bind函数是通过传值来进行绑定的.

bindobject的参数都是引用传入

1.lambda 表达式比std::bind可读性更好, 更具有表达力, 更有效率(更有机会inline)

2.在C++ 11的情况下, std::bind可以帮助实现move capture或者绑定有模板化函数调用的运算符. 比如 operator()(T&&)什么的

 

35.比起基于线程的编程,更喜欢基于任务的编程

1.std::thread 无法直接获取函数返回的值,而且如果这个函数抛出异常程序直接终止

2.基于thread的编程调用需要 手动管理 thread exhaustion, 过度提交thread,加载平衡,以及适应新平台

3.使用std::async和默认的启动策略并基于task的编程手段会帮你解决大多数以上提到的问题

4.当然在某些场合(你是大佬)下,还是要用thread:

需要调用其他实现了thread的API比如windows的

 

你需要而且还能够优化你的应用里的线程usage

 

你需要自己实现C++ 并发API之外的线程技术.

除此之外菜鸡选手还是用task吧

 

36.当同步性是我们的需求的时候, 特化std::launch::async

延迟策略:除非你要我返回或者等我结果不然我就不运行,只要我运行就会堵塞

异步策略: 异步运行

 

无法预测f是否和t同步运行因为f有可能会被安排延迟运行

无法预测f是否和调用wait和get的线程在同一个thread上

无法预测f根本在运行没有

 

使用默认策略std::async的线程任务需要满足以下条件:

1. 任务不要和调用get和wait的线程并发

2. 哪个线程的变量读取写入并不重要

3. std::async的返回值未来必定会被检查返回值或者这个task永远都不运行也ok

4. 使用wait_for 或者 wait_until 有考虑到deferred status

 

1. std::async 默认政策允许异步和同步的任务执行

2. 当接触线程本地变量时灵活度也带来了不确定性,这种灵活度也可能会导致任务永远不会执行,也影响基于time-out的wait函数调用

3. 如果异步任务执行是有必要的,那么特定化std::launch::async的策略以及参数是很有必要的

37.std::threads 在任何路径上都是unjoinable

1.使std::thread在所有路径上都是不可加入的

2.destruction时join会导致很难debug的性能异常

3.destruction时join会导致很难debug的未定义行为

4. 把std::thread object定义在数据成员列表最后

 

38.当心各种线程handle的析构行为

future的析构器只销毁future object的数据成员

最后一个引用共享状态并且与一个非延迟task关联并且使用async的启动策略的future object 会阻塞,直到task 完成

 

39.考虑用void futures给one-shot 事件通信

1. condvar_based的设计需要额外的mutex,需要限制检测和反应task之间的相对进程,并且需要反应线程进行验证所需要的event是否发生

2. 启用flag可以减少消耗,但是这是基于轮询,不是阻塞

3.condvar_baed 和 flag方法可以合并在一起使用,但是产生的通信机制太僵硬

4.使用std::promise和futures可以避免这些问题,但是这个方法使用的共享状态是堆空间上的,但只是一次性通讯哟

 

40.并发用std::atomic,特殊内存用volatile 

std::atomic把一个数据的存取变成原子操作,也就是不会产生数据竞争:并发软件

volatile 指的是写和读不会被优化,是给特殊内存用的

 

41.对于可复制的参数,考虑按值传递,前提是这些参数移动起来很便宜,而且总是可以复制

1. 对于经常被复制并且可被复制,易于移动的参数,传值和传引用是差不多高效的

2. 通过构造函数创建object比copy和assignment贵的多

3. 传值容易引起切割问题,所以对于基类参数类型不太好使用传值

 

42.考虑emplace而不是insertion

1. 一般来讲emplacement要比insertion要高效

2. 快的时候:

被添加的value是被constructed 到容器里的,不是assigned

传入的参数类型和声明类型不同

容器不拒绝重复值

3. emplacement可以整一些类型转换,但这些用insertion整就会拒绝你的code。

posted @ 2021-07-06 09:19  Tonarinototoro  阅读(271)  评论(0编辑  收藏  举报