所有由qicosmos发布的文章

一个更好的自动注册工厂

在几年前我介绍过一种C++11实现的自动注册工厂,这是工厂模式的一种优雅的实现。在这里我们需要明确一个概念就是工厂模式,它是如何优雅地解决一个产品族的创建问题。所谓产品族就是一个继承体系的产品,比如有一个产品Message,它是一个基类,有很多Message是从它派生而来的,比如有Message1,Message2,Message3…等很多产品。

这些产品的创建依赖于某个key,类似于这样:

这是一个典型的工厂方法,这种写法在产品不多的时候是没问题的,但是如果产品越来越多的时候,switch-case就会越来越长,导致难以维护。另外还存在一个问题,有的产品不是无参数的构造函数,如果有些产品依赖了不同的参数,那么这个工厂方法是无法满足需求的。

之前介绍的自动注册工厂解决了switch-case膨胀的问题,但是对于需要参数的产品的创建没有解决得很好,需要进一步改进。改进的办法是把参数作为factory的模板参数,这样就可以解决有参数需求的问题了。

下面是具体实现:

这是一个Messgae产品族的工厂类,有了这个类之后我们就可以很方便地创建各种产品了。下面是测试例子:

Message产品族有4类产品,有的是无参的,有的是多个参数的产品,现在都可以统一创建了,直接输入key和构造参数即可,这个key可以自行修改为int或者枚举变量。

需要注意的是msg4,因为它注册的时候提供了一个function,让function提供创建功能,以满足更灵活地需求。

有了这样一个工厂类之后我们就可以很好地解决产品族创建的问题了。你还可以基于此把它改成一个抽象工厂类,但我觉得你应该慎重考虑一下是否有必要,一般情况下工厂模式就够了,不需要引入更多的复杂性。

tensorflow variant源码分析

tensorflow variant基本语义

通过分析tf.variant的源码可以知道它其实是一个any语义,即这个类型可以被任意类型赋值,它的主要目的就是做彻底的类型擦除。

这个名字取得有迷惑性,它和标准库和boost库中的variant语义是不一样的,而是和c++17中的std::any对应的。

tf.variant用法

可以从tf的测试代码中知道它的基本用法,用法很简单:

从这个测试代码中可以看到tf.variant和std::any/boost.any用法是差不多的。

tf.variant内部有一个unique_ptr指针, 默认为nullptr,所以没有赋值的时候总是返回nullptr。

Int(42)赋值给variant之后,就可以通过get(x)来获得Int指针了,接着就可以得到其实际的Int值了;因为这个Variant是any语义,所以任意类型比如Tensor也可以复制给它,取值方法也是类似的,先取T指针,接着调用value得到初始值。

注意,这里的get中类型必须和赋值时候的类型一致,不一致的时候会返回nullptr(除了void,传void时会返回void*指针);

any和variant比较

之前介绍过vairant,知道它也是用来做类型擦除的,不过variant擦除的类型是有限个的,必须要事先指定,它只能实现部分的类型擦除。

而any也是实现类型擦除的,它可以代表任何类型,不需要像variant那样需要事先确定擦除的类型,看起来更方便更强大了。

事实上这个any并不是变得更强大了,它虽然能彻底擦除类型,但是,在取值的时候需要知道准确的类型,这反倒不如variant方便了,variant在取值的时候通过一个visitor是不需要知道具体类型的。

二者的本质区别是类型擦除方式不一样,variant是通过栈上的一个定长内存块去保存赋值对象的,variant中所有的类型共用这块buffer,而any是通过派生,在堆内存上创建一个实际的对象,并且每次赋值都会析构之前的对象,重新在堆内存上重新创建一个新对象。

所以从性能上说variant的效率是高于any的,二者效率大约相差4倍左右。 附一个测试代码:

所以能用variant就不用any,除非真的需要擦除所有的类型,或者是为了追求代码更简单不太在意性能的时候可以用any,大部分情况下用variant擦除部分类型就可以满足需求了。

any的一个典型应用场景就是http服务器中的session,这个session是保存用户在服务器端的数据的,这个数据类型可能是字符串,数值,或者用户自定义的对象,在这种情况下无法定义一个variant,这时候用any是合适的。

tf.variant的实现

tf.variant的实现思路

tf.variant的实现思路实际上就是any的实现思路,any对象内部有一个内部基类的unique_ptr,这个基类有一个带模版参数的派生类。

这个派生模版类是泛型的,用它代表所有类型。

当给any对象赋值的时候,我们就创建这个派生类对象,这个对象保存了实际的类型。

在后面取值的时候我们就可以将这个基类向下转型为某个具体的派生类对象了,这时需要判断当前保存的对象类型和传入的类型是否一致,不一致则返回nullptr。

any

tf.variant的源码实现

1.内部定义一个抽象类和一个派生模版类

2.定义一个成员变量,内部接口类的指针–std::unique_ptr< ValueInterface > value_;

3.赋值时创建派生类对象

赋值的时候对赋值类型做了限定,必须为非Variant的,并且有拷贝构造函的类型,因为any内部会重新创建这个对象,之后赋值给基类指针value_,同时实现了类型擦除。

4.取值

取值的时候会判断是否初始化以及类型是否匹配,满足条件就转成实际的类型,否则返回nullptr。

这个代码的实现还是比较简单的,就是实现了一个基本的any语义,还有一些自己扩展的部分,比如encode和decode之类的。

总结

tf.variant本质上就是一个any,用来做类型擦除,像c#,java中的object那样代表一个通用类型。

在tensorflow中很多时候是作为一个函数参数,有些这样代码:

目前还没进一步分析tf的代码,我猜测这样做的目的可能是为了统一的接口以及灵活性,接口可以保持通用不变,具体实现将因具体类型不同而各有不同,方便扩展。

variant原理和应用

variant原理和应用

variant语义

variant是一个泛化的、类型安全的union。可以保存类型不同的对象,它的特点是多类型单值。

基本用法

以c++17中的variant为例(boost中的variant和标准库的用法几乎一样),我们定义一个这种的variant:

std::variant<int, double, char> v;

这个variant可以用来存储int, double和char三种类型的数据,所以我们可以这样赋值:

可以看到类型的值可以赋给同一个variant,如果将一个非int, double和char的类型赋值给v的话则在会出现一个编译期错误,所以variant是类型安全的,variant只允许将定义时指定的那些类型赋值给它。注意,重新赋值的时候之前的对象会自动析构掉。

接下来看如何取值:

通过std::get(v)就可以获取对应的值了,不过取值的时候需要知道当前的variant具体是什么类型,如果类型不对则会抛异常。

这种需要传具体类型的访问variant的方法有局限性,有时候我们不能确切知道当前的具体类型,这种情况下该如何取值呢?
以boost库提供的访问vairant方法为例:

这种访问方法需要定义一个函数对象对每个类型的访问都写一个重载函数,如果找不到对应的重载函数则会报一个编译期错误。

这种方法更常用,因为它不需要知道具体的类型。

还可以用C++11写一个支持lambda的visitor,避免写一个函数对象,用起来可以更简单。

实现原理

虽然很多库自己实现了variant,但其实现原理是类似的,实现variant主要分为下面几步:

1.variant内部定义一个足够大的缓冲区。

足够大的意思是这个缓冲区可以用来创建variant定义时的那些类型,具体多大呢,需要借助模版元方法遍历所有的类型找出其中size最大的那个类型,找到这个最大的size之后我们就可以定义一个栈上的char数组了,这里还需要考虑内存对齐的问题。

2.通过placement new创建某一个类型的对象。

这个对象就在内部的栈上缓冲区中创建的,所有的类型共用这块缓冲区,和union类似。具体创建对象的时候用的是placement new,一个是出于性能考虑,一个出于便于回收之前的对象考虑,因为重新赋值的时候需要将之前的对象析构掉。

3.variant赋值。

赋值时需要保存当前类型的type_index, 便于后面取值的时候判断需要取值的类型和当前类型是否一致。

4.variant的析构。

析构的时候要根据type_index遍历所有的类型找到当前的类型然后调用该类型的析构函数去析构。

通过这个思路就可以实现一个基本的variant,如果需要支持更多功能的时候还需要增加一些代码,比如支持嵌套的variant之类,支持lambda访问等等。

具体的实现可以参考我的github上C++11实现的基本功能的variant:https://github.com/qicosmos/cosmos/blob/master/Variant.hpp.

应用场景

我们一般在什么场景下需要用到variant呢?varaint一般用于类型擦除,比如我们需要访问一个某个值,这个值的类型是有限个的,这时可以用variant来表示这个“可能是多个类型的值”,避免针对具体类型去写代码,可以消除强制转换,或者把复杂问题简化,比如一些异构类型没办法抽象泛化的时候,用variant泛化是很方便的。

一个典型的应用场景就是数据库表访问的场景,数据库表字段的类型是有限多个的,我们完全可以用一个variant来表示数据库字段,接着就可以像通用类型那样去访问数据表字段了,而不必关注具体的类型,把一个复杂问题变得很简单了。

如何引入到我们的工程

boost库中已经有variant了,c++17中也引入了,如果我们的编译器还只支持C++11,则可以直接用boost中的variant,boost中的variant是header only的,直接包含头文件既可以,这里涉及到另外一个问题就是如何引入boost库。

boost库是一个广泛使用具有工业强度的c++库,很多C++新标准里的新特性都是来自于boost,使用里面已经有的库,可以避免重复造轮子,节省开发测试时间,而且质量是有保证的。

那么如何引入boost呢,引入boost有两种方式:

1.直接安装

可以查看当前源中有哪个boost库, 然后直接安装:

安装之后直接在工程中引用boost头文件既可以,以variant为例,使用variant时直接include即可:

2.编译安装

从boost.org官网下载一个版本,解压之后在boost目录执行两个命令就可以了

总结

variant作为一个类型安全的union,可以帮助我们做类型擦除,从而可以避免强制转换,便于编写更加泛化通用的代码,化繁为简。

cinatra–一个高效易用的c++ http框架

目录

cinatra简介

cinatra是一个高性能易用的http框架,它是用modern c++(c++17)开发的,它的目标是提供一个快速开发的c++ http框架。它的主要特点如下:

  1. 统一而简单的接口
  2. header-only
  3. 跨平台
  4. 高效
  5. 支持面向切面编程

cinatra目前支持了http1.1/1.0和websocket, 你可以用它轻易地开发一个http服务器,比如常见的数据库访问服务器、文件上传下载服务器、实时消息推送服务器,你也可以基于cinatra开发一个mqtt服务器。

如何使用

编译依赖

cinatra是基于boost.asio开发的,所以需要boost库,同时也需要支持c++17的编译器,依赖项:

  1. boost.asio, boost1.66
  2. c++17编译器(gcc7.2,clang4.0, vs2017 update15.5)

使用

cinatra是header-only的,直接引用头文件既可。

快速示例

示例1:一个简单的hello world

5行代码就可以实现一个简单http服务器了,用户不需要关注多少细节,直接写业务逻辑就行了。

示例2:展示如何取header和query以及错误返回

示例3:面向切面的http服务器

本例中有两个切面,一个校验http请求的切面,一个是日志切面,这个切面用户可以根据需求任意增加。本例会先检查http请求的合法性,如果不合法就会返回bad request,合法就会进入下一个切面,即日志切面,日志切面会打印出一个before表示进入业务逻辑之前的处理,业务逻辑完成之后会打印after表示业务逻辑结束之后的处理。

示例4:文件上传

cinatra目前支持了multipart和octet-stream格式的上传。

multipart文件上传

短短几行代码就可以实现一个http文件上传的服务器了,包含了异常处理和错误处理。

octet-stream文件上传

示例5:文件下载

示例6:websocket

性能测试

测试用例:

ab测试:ab -c100 -n5000 127.0.0.1:8080/

服务器返回一个hello。

在一个8核心16G的云主机上测试,qps在9000-13000之间。

对比测试

通过ab测试和boost.beast做对比,二者qps相当,大概是因为二者都是基于boost.asio开发的的原因。cinatra目前还没做专门的性能优化,还有提升空间。

注意事项

文件上传下载,websocket的业务函数是会多次进入的,因此写业务逻辑的时候需要注意,推荐按照示例中的方式去做。

cinatra目前刚开始在生产环境中使用, 还处于完善阶段,可能还有一些bug,因此不建议现阶段直接用于生产环境,建议先在测试环境下试用。

试用没问题了再在生产环境中使用,试用过程中发现了问题请及时提issue反馈或者邮件联系我。

测试和使用稳定之后cinatra会发布正式版。

roadmap

  1. 支持ssl
  2. 支持断点续传
  3. 支持session和cookie
  4. 接口优化、性能优化

我希望有越来越多的人使用并喜欢cinatra,也希望cinatra在使用过程中越来越完善,变成一个强大易用、快速开发的http框架,欢迎大家积极参与cinatra项目,可以提issue也可以发邮件提建议,也可以提pr,形式不限。

这次重构的cinatra几乎是重写了一遍,代码比之前的少了30%以上,接口统一了,http和业务分离,具备更好的扩展性和可维护性。

联系方式

purecpp@163.com

http://purecpp.org/

https://github.com/qicosmos/cinatra

致谢

感谢社区的“逐雁南飛”和“非常可乐”两位朋友的帮助,你们帮我澄清了一些http业务细节,同时也提出了一些宝贵意见,在此致以衷心的感谢!

一个好用的短信发送的客户端

背景

最近需要做一个登录短信验证的功能,看了不少短信平台,对于c++接口的支持不友好,有的甚至没有,有的是通过curl去实现的,还是同步的,比较难用。于是就基于boost.asio用c++17实现了一个异步的短信发送客户端, 非常好用,只有一个发送短信的接口。
现在工程中的例子是以云片短信平台为例子的,你可以很容易修改为其他的短信平台。

特点

sms_client的主要特点:

  1. header only
  2. async
  3. easy to use

快速示例

一个header only的c++ 日志库

背景

NanoLog是一个非常小巧的log库,代码很少,不到一千行,速度比spdlog还快,应用上也能满足需求,我很喜欢。但是也存在一些不足,比如日志文件的数量没有限制,每次重启之后会从头开始写等等问题,还需要进一步完善。于是我新建了一个工程nanolog,这个工程继承于nanolog,将原工程改成header only,并用了一些最新的特性来简化原来的代码。

快速示例

如何编译

由于使用了C++17的新特性,所以需要支持C++17的编译器,gcc7.2,vs2017 15.5

roadmap

  1. 增加文件数量上限
  2. 重新写日志从上次的位置继续写

运行期的tuple get

运行期的tuple get

背景

通过索引来访问tuple的元素可以通过std::get(tuple)实现,不过这个I必须是编译期常量,能否通过运行期的一个索引来访问tuple的元素呢?
刚好最近有这个需求,用C++17的fold expression可以比较容易实现根据运行期索引来访问tuple的元素。

实现

根据运行期索引访问tuple

测试代码:

将打印第二个元素。

我们可以用hack一点的写法来提高效率:

这种写法一行代码实现,不需要再多写一个子函数了。fold expression的时候通过一个bool表达式来控制是否调用函数,不是每次都进入调用函数,只有在条件满足的时候才进入,之前的写法是每次都会进入子函数,在子函数中做判断是否调用目标函数。这种写法效率更高。

性能比较

visit的第二种写法效率比第一种写法效率高了2-2.5倍,应该是编译器对这种写法做了优化。下面是测试代码:

输出时间:

C++17实现的一个简单的redis客户端

motiviation

实现最常用的redis操作,只支持数字类型字符串类型(包括std::string, c字符串,和字符串数组),简单好用。

基本接口

有这三个接口后用户就可以很方便地使用redis了。

注意:get接口会抛异常,当字符串转换为对应的值失败时会抛异常,当要取的k-v不存在时也会抛异常。

基本用法

roadmap

  1. 增加conneciton pool
  2. 增加更多redis访问接口

ormpp使用文档

目录

ormpp的目标

ormpp最重要的目标就是让c++中的数据库编程变得简单,为用户提供统一的接口,支持多种数据库,降低用户使用数据库的难度。

ormpp的特点

ormpp是modern c++(c++11/14/17)开发的ORM库,目前支持了三种数据库:mysql, postgresql和sqlite,ormpp主要有以下几个特点:

  1. header only
  2. cross platform
  3. unified interface
  4. easy to use
  5. easy to change database

你通过ormpp可以很容易地实现数据库的各种操作了,大部情况下甚至都不需要写sql语句。ormpp是基于编译期反射的,会帮你实现自动化的实体映射,你再也不用写对象到数据表相互赋值的繁琐易出错的代码了,更酷的是你可以很方便地切换数据库,如果需要从mysql切换到postgresql或sqlite只需要修改一下数据库类型就可以了,无需修改其他代码。

快速示例

这个例子展示如何使用ormpp实现数据库的增删改查之类的操作,无需写sql语句。

如何编译

编译器支持

需要支持C++17的编译器, 要求的编译器版本:linux gcc7.2, clang4.0; windows >vs2017 update5

数据库的安装

因为ormpp支持mysql, postgresql和sqlite,所以需要安装mysql,postgresql,postgresql官方提供的libpq以及sqlite3,安装之后,在CMakeLists.txt配置目录和库路径。

上面两步完成之后就可以直接编译了。

接口介绍

ormpp屏蔽了不同数据库操作接口的差异,提供了统一简单的数据库操作接口,具体提供了数据库连接、断开连接、创建数据表、插入数据、更新数据、删除数据、查询数据和事务相关的接口。

接口概览

具体的接口使用介绍

先在entity.hpp中定义业务实体(和数据库的表对应),接着定义数据库对象:

  1. 连接数据库

返回值:bool,成功返回true,失败返回false.

  1. 断开数据库连接

注意:用户可以不用显式调用,在数据库对象析构时会自动调用disconnect接口。

返回值:bool,成功返回true,失败返回false.

3.创建数据表

create_datatable example:

注意:目前只支持了key和not null属性,并且只支持单键,还不支持组合键,将在下一个版本中支持组合键。

返回值:bool,成功返回true,失败返回false.

4.插入单条数据

返回值:int,成功返回插入数据的条数1,失败返回INT_MIN.

5.插入多条数据

返回值:int,成功返回插入数据的条数N,失败返回INT_MIN.

  1. 更新单条数据

注意:更新会根据表的key字段去更新,如果表没有key字段的时候,需要指定一个更新依据字段名,比如

[/crayon]

返回值:int,成功返回更新数据的条数1,失败返回INT_MIN.

5.插入多条数据

注意:更新会根据表的key字段去更新,如果表没有key字段的时候,需要指定一个更新依据字段名,用法同上。

返回值:int,成功返回更新数据的条数N,失败返回INT_MIN.

  1. 删除数据

返回值:bool,成功返回true,失败返回false.

7.单表查询

返回值:std::vector,成功vector不为空,失败则为空.

8.多表或特定列查询

返回值:std::vector<std::tuple>,成功vector不为空,失败则为空.

9.执行原生sql语句

注意:execute接口支持的原生sql语句是不带占位符的,是一条完整的sql语句。

返回值:int,成功返回更新数据的条数1,失败返回INT_MIN.

10.事务接口

开始事务,提交事务,回滚

返回值:bool,成功返回true,失败返回false.

11.面向切面编程AOP

定义切面:
struct log{
//args…是业务逻辑函数的入参
template<typename… Args>
bool before(Args… args){
std::cout<<“log before”<<std::endl;
return true;
}

[/crayon]

注意:切面的定义中,允许你只定义before或after,或者二者都定义。

roadmap

  1. 支持组合键。
  2. 多表查询时增加一些诸如where, group, oder by, join, limit等常用的谓词,避免直接写sql语句。
  3. 增加日志
  4. 增加获取错误消息的接口
  5. 支持更多的数据库

联系方式

purecpp@163.com

http://purecpp.org/

https://github.com/qicosmos/ormpp

Copy Protected by Chetan's WP-Copyprotect.