1 |
|
阅读spdlog-rotating_file_sink源码
thread_pool 源码学习
rotating_file_sink定义
1 | // |
构造函数
1 |
|
rotate_()函数
1 | /// @name rotate_ |
虚函数的实现
1 | /// @name sink_it_ |
C++异常处理
异常处理
异常处理(exception handling
)机制允许程序独立开发的部分能够在运行时就出现问题进行通信并作出相应的处理。异常是的我们能够将问题的检测和解决过程分离开来。程序的一部分负责检测问题的出现,然后解决该问题的任务传递给程序的另一部分。检测环节无需知道问题处理模块的所有细节,反之亦然。
1. 抛出异常
在C++语言中,我们通过抛出(throwing
)一条表达式来引发(raised
)一个异常。被抛出的表达式的类型以及当前的调用链共同决定了哪段处理代码(handler
)将被用来处理该异常。被选中的处理代码实在调用链中与抛出对象类型匹配的最近的处理代码。其中,根据抛出对象的类型和内容,程序的异常抛出部分会告知异常处理部分到底发生了什么错误。
当执行一个throw
时,跟在throw
后面的语句将不再被执行。相反,程序的控制权从throw
转移到与之匹配的catch
模块。该catch
可能是同一函数中的局部catch
,也可能位于直接或间接调用了发生异常的函数的另一个函数中。控制权从一处转移到另一处,这有两个重要的含义:
- 沿着调用链的函数可能会提早退出。
- 一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁。
因为跟在throw
后面的语句将不再被执行,所以throw
语句的有类似于return
语句:它通常作为条件语句的一部分或者作为某个函数的最后(或者唯一)一条语句。
1.1 栈展开
当抛出一个异常后,程序暂停当前函数的执行过程并立即开始寻找与异常匹配的catch
子句。
-
当
throw
出现在一个try语句块(try block
)内时,检查与该try
块关联的catch
子句。 -
如果找到了匹配的
catch
,就使用该catch
处理异常。 -
如果这一步没找到匹配的
catch
且该try
语句嵌套在其他try
块中,则继续检查与外层try
匹配的catch
子句。 -
如果还是找不到匹配的
catch
,则退出当前函数,在调用当前函数的外层函数中继续寻找。 -
如果对抛出异常的函数的调用语句位于一个
try
语句块内,则检查与该try
块关联的catch
子句。 -
如果找到了匹配的
catch
,就使用该catch
处理异常。 -
否则,如果该
try
语句嵌套在其他try
块中,则继续检查与外层try
匹配的catch
子句。 -
如果仍然没找到匹配的
catch
,则退出当前这个主调函数,继续在调用刚刚退出的这个函数的其他函数中寻找,以此类推。
上述过程被称为栈展开(stack unwinding
)过程。栈展开过程沿着嵌套函数的调用链不断查找,直到找到了与异常匹配的catch
子句为止;或者也可能一致没找到匹配的catch
,则退出主函数后过程中止。
假设找到了一个匹配的catch
子句,则程序进入该子句并执行其中代码。当执行完这个catch
子句后,找到与try
块关联的最后一个catch
子句后的点,并从这里继续执行。
如果没有找到匹配的catch
子句,程序将退出。因为异常通常被认为是妨碍程序正常执行的事件,所以一旦引发了某个异常,就不能对它置之不理。当找不到匹配的catch
时,程序将调用标准库函数terminate
,顾名思义,terminate
负责中止程序的执行过程。
1.2 栈展开过程中对象被自动销毁
在栈展开过程中,位于调用链上的语句块可能会提前退出。如果在栈展开过程中退出了某个块,编译器将负责确保在这个块中创建的对象都能被正确的销毁。如果某个局部对象的类型是类类型,则该对象的析构函数将被自动调用。与往常一样,编译器在销毁内置类型的对象时不需要做任何事情。
如果异常发生在构造函数中,则当前的对象可能只构造了一部分。有的成员已经开始初始化了,而另外一些成员在异常发生前也许还没有开始初始化。即使某个对象只构造了一部分,我们也要确保构造的成员能被正确的销毁(否则会发生内存泄露)。
类似的,异常也可能发生在数组或标准库容器的元素初始化过程中。与之前类似,如果在异常发生前已经构造了一部分元素,则我们应该确保这部分元素被正确的销毁。
1.3 析构函数与异常
析构函数总是会被执行的,但是函数中负责释放资源的代码却可能会被跳过。如果一个块分配了资源,并且在负责释放这些资源的代码前面发生了异常,则释放资源的代码将不会被执行。另一方面,类对象分配的资源将由类的析构函数负责释放。因此,如果我们使用类来控制资源的分配,就能确保无论函数正常结束还是遭遇异常,资源都能被正确地释放。(RAII
的思想,在构造函数中获取资源(i.e new
),在析构函数中释放资源(i.e delete
)。)
所以出于栈展开可能使用析构函数的考虑,析构函数不应该抛出不能被它自身处理的异常。换句话说,如果析构函数需要执行某个可能抛出异常的操作,则该操作应该被放置在一个try语句块当中,并且在析构函数内部得到处理(如果不这样做的话,程序会马上被终止)。
注:所有标准库类型都能保证它们的析构函数不会引发异常。
1.4 异常对象
异常对象(exception object
)是一种特殊的对象,编译器使用异常抛出表达式来对异常对象进行拷贝初始化。因此throw
语句中的表达式必须拥有完整类型。而且如果该表达式是类类型的话,则相应的类必须含有一个可访问的析构函数和一个可访问的拷贝或移动构造函数。如果该表达式是数组类型或函数类型,则表达式将被转换成与之对应的指针类型。
异常对象位于有编译器管理的空间中,编译器确保无论调用哪个catch
子句都能访问该空间。异常处理完毕后,异常对象被销毁。
当一个异常被抛出是,沿着调用链的块将依次退出直至找到与异常匹配的处理代码。如果退出某个块,则同时释放块中局部对象使用的内存。因此,抛出一个指向局部对象的指针几乎肯定是一种错误行为。如果指针所指的对象位于某个块中,而该块在catch
语句之前就已经退出了,则意味着在执行catch
语句之前局部对象已经被销毁了。
当我们抛出一条表达式时,该表达式的静态编译时类型决定了异常对象的类型。很多情况下程序抛出的表达式类型来自于某个继承体系。如果一条throw
表达式解引用一个基类指针,而该指针实际指向的是派生类对象,则抛出的对象将被切掉一部分,只有基类部分被抛出。
注: 抛出指针要求在任何对应处理代码存在的地方,指针所指的对象都必须存在。
2. 捕获异常
catch
子句(catch clause
)中的一场声明(exception declaration
)看起来像是只包含一个形参的函数形参列表。像在形参列表中一样,如果catch
无须访问抛出的表达式的话,则我们可以忽略捕获形参的名字。
声明的类型决定了处理代码所能捕获的异常类型。这个类型必须是完全类型,它可以是左值引用,不能是右值引用。当进入一个catch
语句后,入参通过异常对象初始化异常声明中的参数。和函数的参数类似,如果catch
的参数类型是非引用类型,则该参数是异常对象的一个副本,如果参数是引用类型,则和其他引用参数一样,该参数是异常对象的一个别名。
如果catch
的参数是基类类型,则我们可以使用其派生类类型的异常对象对其进行初始化。此时,如果catch
的参数是非引用类型,则异常对象将被切掉一部分,如果catch
的参数是基类的引用,则该参数将以常规方式绑定到异常对象上。
最后一点需要注意的是,异常声明的静态类型将决定catch
语句所能执行的操作。如果catch
的参数是基类类型,则catch
无法使用派生类特有的任何成员。
Tips: 通常情况下,如果
catch
接收的异常与某个继承体系有关,则最好将该catch
的参数定义成引用类型。
2.1 查找匹配的处理代码
在搜寻catch
语句的过程中,我们最终找到的catch
未必是异常的最佳匹配。相反,挑选出来的应该是第一个与异常匹配的catch
语句。因此,越是专门的catch
越应该置于整个catch
列表的前端。
因为catch
语句是按照其出现的顺序逐一匹配的,所以当程序员使用具有继承关系的多个异常时必须对catch
语句的顺序进行组织管理,是的派生类异常的处理代码出现在基类异常的处理代码异常之前。
与实参和形参的匹配规则相比,异常和catch
异常声明的匹配规则受到更多限制。此时,绝大多数类型转换都不被允许,除了一些极细小的差别之外,要求异常的类型和catch
声明的类型时精确匹配的:
- 允许从非常量的类型转换,也就是说一条非常量对象的
throw
语句可以匹配一个接受常量引用的catch
语句 - 允许从派生类向基类的类型转换。
- 数组被转换成指向数组(元素)类型的指针,函数被转化成指向该函数类型的指针。
除此之外,包括标准算术类型转换和类类型转换在内,其他所有转换规则都不能在匹配catch的过程中使用。
如果在多个catch语句的类型之间存在着继承关系,则我们应该把继承链最低端的类(
most derived type
)放在前面,而将继承链最顶端的类(least derived type
)放在后面。
2.2 重新抛出
一个单独的catch
语句不能完整的处理某个异常。在执行了某些校正操作之后,当前的catch
可能会决定由调用链更上一层的函数接着处理异常。一条catch语句通过重新抛出的操作将异常传递给另外一个catch
语句。这里的重新抛出仍然是一条throw
语句,只不过不包含任何表达式: throw;
空的throw
语句只能出现在catch
语句或catch
语句直接或间接调用的函数之内。如果在处理代码之外的区域遇到了空throw
语句,编译器将调用terminate
。
一个重新抛出语句并不指定新的表达式,而是将当前的异常对象沿着调用链向上传递。
很多时候,catch语句会改变其参数内容。如果在改变了参数的内容后catch语句重新抛出异常,则只有当catch异常声明是引用类型时我们对参数所作的改变才会被保留并继续传播。
2.3 捕获所有异常的处理代码
为了一次性捕获所有异常,我们使用省略号作为异常声明,这样的处理代码称为捕获所有异常的处理代码,形如catch(...)
.
catch(...)
通常与重新抛出语句一起使用,其中catch
执行当前局部能完成的工作,随后重新抛出异常。
Tips: 如果
catch(...)
与其他几个catch
语句一起出现,则catch(...)
必须在最后的位置。出现在捕获所有一场语句后面的catch
语句将永远不会被匹配。
3. 函数try语句块与构造函数
通常情况下,程序执行的任何时刻都可能发生异常,特别是一场可能发生在处理构造函数初始值的过程中。构造函数在进入其函数体之前首先执行初始值列表。因为在初始值列表抛出异常时构造函数体内的try
语句块还未生效,所以构造函数体内的catch
语句无法处理构造函数初始值列表抛出的异常。
要想处理构造函数初始值抛出的异常,我们必须将构造函数写成函数try语句块(function try block
)的形式。函数try
语句使得一组catch
语句既能处理构造函数体(或析构函数体),也能处理构造函数的初始化过程(或析构函数的析构过程)。
1 | template <typename T> |
4. noexcept 异常说明
1 | void recoup() noexcept; /** 不会抛出异常 */ |
-
对于一个函数来说,noexcept说明要么出现在该函数的所有声明语句和定义语句中,要么一次也不出现。该说明应该在函数应该在函数的尾置返回类型之前。
-
我们也可以在函数指针的声明和定义中指定
noexcept
。 -
在typedef或类型别名中则不能出现
noexcept
。 -
在成员函数中,
noexcept
说明符需要跟在const
及引用限定符之后,而在final
、override
或虚函数=0
之前。
4.1 违反异常说明
编译器并不会在编译时检查noexcept
说明。实际上,如果一个函数说明了noexcept
的同时又含有throw
语句或者调用了可能抛出异常的其他函数,编译器将顺利通过,并不会因为这种违反异常说明的情况而报错。
因此可能会出现一种情况:尽管函数说明了它不会抛出异常,但实际上还是抛出了。一旦一个noexcept
函数抛出异常,程序就会调用terminate
以确保遵守不在运行时抛出异常的承诺。
上述过程是执行栈展开未作约定,因此noexcept
可以用在两种情况下:一是我们确认函数不会抛出异常,二是我们根本不知道该如何处理异常。
4.2 noexcept运算符
noexcept
说明符接受一个可选实参,该实参必须能转换为bool
类型:如果实参是true
,则函数不会抛出异常;如果实参是false
,则函数可能抛出异常:
1 | void recoup() noexcept(true); /** 不会抛出异常 */ |
noexcept
说明符的实参常常与noexcept
运算符混合使用。noexcept
运算符是一个一元运算符,它的返回值是一个bool类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。和sizeof
类似,noexcept
也不会求其运算对象的值。
1 | noexcept(recoup()) /** 如果recoup不跑出异常则结果为true;否则结果为false */ |
我们可以使用noexcept运算符得到如下的异常说明:
1 | void f() noexcept(noexcept(g())); // f 和 g的异常说明一致 |
如果函数g()
承诺了不会抛出异常,则f也不会抛出异常;如果g()
没有异常说明符,或者g虽然有异常说明符但是允许抛出异常,则f()
也可能抛出异常。
noexcept
有两层含义:当跟在函数参数列表后面时它是异常说明符;而当作为noexcept
异常说明的bool
实参出现时,它是一个运算符。
4.3 异常说明与指针、虚函数和拷贝控制
函数指针及该指针所指的函数必须具有一致的异常说明。也就是说我们为某个指针做了不抛出异常的声明,则该指针将只能指向不抛出异常的函数。相反,如果我们显式或隐式地说明了指针可能抛出异常,则该指针可以指向任何函数,即使是承诺了不抛出异常的函数也可以。
如果虚函数承诺了它不会抛出异常,则后续派生出来的虚函数也必须做出同样的承诺;与之相反如果基类的虚函数允许抛出异常,则派生类的对应函数既可以允许抛出异常,也可以不允许抛出异常。
当编译器合成拷贝控制成员时,同时也生成一个异常说明。如果对所有成员和基类的所有操作都承诺了不会抛出异常,则合成的成员是noexcept
的。如果合成成员调用的任意一个函数可能抛出异常,则合成的成员是noexcept(false)
。而且如果我们定义了一个析构函数但是没有为它提供异常说明,则编译器将合成一个。合成的异常说明将于假设有编译器为类合成析构函数时所得的异常说明一致。
C++20三路比较符
三路比较符(C++20)
例子1:
1 | #include <iostream> |
例子2:
1 | #include <iostream> |
C++严格弱序的介绍
严格弱序(strict weak ordering)
关联式容器(set
、multiset
、map
和multimap
)的排序准则的定义,和std::sort的排序准则定义必须遵守严格弱序,详细描述见官方解释(strict weak ordering.pdf)。
严格弱序的定义:
简单的来说就是a<b返回true,a=b和a>b返回false。
详细定义:
必须是非对称的(antisymmetric)。
对
operator<
而言, 如果x < y为true, 则y < x为false。对判断式(predicate)
op()
而言,如果op(x, y)为true,则op(y, x)为false。必须是可传递的(transitive)。
对
operator<
而言,如果x < y 为true且y < z为true, 则x < z 为false。对判断式(predicate)
op()
而言,如果op(x, y)为true且op(y, z)为tru,则op(x, z)为true。
必须是非自反的(irreflexive)
对
operator<
而言,x < x 永远是false对判断式(predicate)
op()
而言,op(x, x)永远是false。必须有等效传递性(transitivity of equivalence)
对
operator<
而言,假如 !(a<b) && !(b<a) 为true且 !(b<c) && !(c<b) 为 true
那么!(a<c) && !(c<a) 也为true.
对判断式(predicate)op()
而言, 假如 op(a,b), op(b,a), op(b,c), 和op(c,b) 都为
false, 那么op(a,c) and op(c,a) 也为false.
1 | // 一个定义std::set<struct>的例子 |
下面举一个会崩溃的例子对二维数组排序。
1 | #include <algorithm> |
两个参数的重载符号简单示例
1 | class key |
1 | class key |
C++关于乘法溢出的判断
概述
首先我们对于乘法溢出的判断,先写测试用例:
由上图我们简化测试用例:
我们可以这样设计乘法溢出函数:
1 | /// 判断两入参相乘是否溢出,溢出返回true,否则返回false |
接下来我们添加测试用例
1 | #include <cassert> |
接下来为特殊数值来添加判断:
添加异号情况的判断:
把函数改为模板,一并添加测试用例:
1 | #include <cassert> |
最后附上完整测试用例:
后记
我们既然有了判断乘法溢出的函数,我们可以借此封装一个带有检查溢出的乘法函数。
1 | #include <optional> |
C++关于虚析构函数
虚析构函数问题
引用标准中原文: 一条有用的方针,是任何基类的析构函数必须为公开且虚, 或受保护且非虚。
虚析构这个概念被设计出来就是为了解决基类指针指向派生类实例的析构问题,当一个基类指针指向派生类实例然后进行delete该指针时,只会执行基类析构函数而派生类的析构函数不会被执行,这将导致派生类构造的资源不会被正确释放,造成内存泄漏。如下示例:
1 | #include <iostream> |
运行结果:
可以看到派生类没有被析构,如要解决该问题在基类析构函数处加上virtual关键字即可。
1 | #include <iostream> |
运行结果:
PlantUML语法学习
PlantUML语法学习
类图
类之间的关系
类之间的关系通过下面的符号定义:
1 | @startuml |
1 | @startuml |
1 | @startuml |
关系上的标识
在关系之间使用标签来说明时, 使用: 后接标签文字。
对元素的说明,你可以在每一边使用"" 来说明.
1 | @startuml |
在标签的开始或结束位置添加< 或> 以表明是哪个对象作用到哪个对象上。
1 | @startuml |
添加方法
为了声明字段(对象属性)或者方法,你可以使用后接字段名或方法名。
系统检查是否有括号来判断是方法还是字段。
1 | @startuml |
也可以使用{} 把字段或者方法括起来
注意,这种语法对于类型/名字的顺序是非常灵活的。
1 | @startuml |
你可以(显式地)使用{field} 和{method} 修饰符来覆盖解析器的对于字段和方法的默认行为
1 | @startuml |
定义可访问性
一旦你定义了域或者方法,你可以定义相应条目的可访问性质。
1 | @startuml |
你可以采用命令(skinparam classAttributeIconSize 0 :)停用该特性
1 | @startuml |
抽象与静态
通过修饰符{static} 或者{abstract},可以定义静态或者抽象的方法或者属性。
这些修饰符可以写在行的开始或者结束。也可以使用{classifier} 这个修饰符来代替{static}.
1 | @startuml |
高级类体
PlantUML 默认自动将方法和属性重新分组,你可以自己定义分隔符来重排方法和属性,下面的分隔符都
是可用的:-- … == __.
还可以在分隔符中添加标题
1 | @startuml |
更多注释
可以在注释中使用部分html 标签:
1 | @startuml |
对象图
对象的定义
使用关键字object 定义实例。
1 | @startuml |
如下图生成:
对象之间的关系
对象之间的关系可以用如下符号定义:
也可以用… 来代替-- 以使用点线。
知道了这些规则,就可以画下面的图:
可以用冒号给关系添加标签,标签内容紧跟在冒号之后。
用双引号在关系的两边添加基数。
1 | @startuml |
如下图生成:
添加属性
用冒号加属性名的形式声明属性。
1 | @startuml |
如下图生成:
活动图
简单活动
使用(*) 作为活动图的开始点和结束点。
有时,你可能想用(*top) 强制开始点位于图示的顶端。
使用–> 绘制箭头。
1 | @startuml |
箭头上的标签
默认情况下,箭头开始于最接近的活动。
可以用[ 和 ] 放在箭头定义的后面来添加标签。
1 | @startuml |
改变箭头方向
你可以使用-> 定义水平方向箭头,还可以使用下列语法强制指定箭头的方向:
- -down-> (default arrow)
- -right-> or ->
- -left->
- -up->
分支
你可以使用关键字if/then/else 创建分支。
1 | @startuml |
不过,有时你可能需要重复定义同一个活动:
1 | @startuml |
更多分支
默认情况下,一个分支连接上一个最新的活动,但是也可以使用if 关键字进行连接。
还可以嵌套定义分支。
1 | @startuml |
注释
你可以在活动定义之后用note left, note right, note top or note bottom, 命令给活动添加注释。
如果想给开始点添加注释,只需把注释的定义放在活动图最开始的地方即可。
也可以用关键字endnote 定义多行注释。
1 | @startuml |
分区
用关键字partition 定义分区,还可以设置背景色(用颜色名或者颜色值)。
定义活动的时候,它自动被放置到最新的分区中。
用} 结束分区的定义。
1 | @startuml |
一个完整的例子
1 | @startuml |
界面格式相关
1 | @startuml |
颜色示例
1 | @startuml |
附注
阅读C++线程池源码
progschj/thread_pool
Github上这个库(progschj/thread_pool)的点赞最多,学习一下。
接口定义
1 | class ThreadPool { |
构造函数和消费者实现
1 | /// @name ThreadPool |
析构函数
1 | inline ThreadPool::~ThreadPool() { |
生产者函数
1 | /// @name enqueue |
C++ 异步运算接口
std::async介绍
下面是一个很好的并行计算的例子。
1 | #include <future> |
std::async中的第一个参数是启动策略,它控制std::async的异步行为,我们可以用三种不同的启动策略来创建std::async
·std::launch::async
保证异步行为,即传递函数将在单独的线程中执行
·std::launch::deferred
当其他线程调用get()来访问共享状态时,将调用非异步行为
·std::launch::async | std::launch::deferred
默认行为。有了这个启动策略,它可以异步运行或不运行,这取决于系统的负载,但我们无法控制它。
见下面例子:
1 | #include <iostream> |
std::promise介绍
std::promise的作用就是提供一个不同线程之间的数据同步机制,它可以存储一个某种类型的值,并将其传递给对应的future, 即使这个future不在同一个线程中也可以安全的访问到这个值
1 | #include <iostream> // std::cout |
std::packaged_task介绍
1 | #include <iostream> // std::cout |
硬件支持的线程数量
由于硬件支持的并行线程数量有限,如果创建线程的数量比硬件支持的数量要多,那么CPU进行的上下文切换可能会浪费大量时间,所以了解硬件支持的线程数量是高效并行编程的重点。
使用std::thread::hardware_concurrency()
来获取硬件支持的线程数量。
1 | #include <iostream> |
std::thread::yield介绍
关于std::thread::yield 和 std::sleep_for的比较
例子:
1 | void worker_thread() { |